Stabilize refunds
This commit is contained in:
@@ -9,15 +9,14 @@ import java.util.List;
|
||||
public class PageResponse<T> {
|
||||
private List<T> content;
|
||||
|
||||
@JsonProperty("number")
|
||||
private int pageNumber;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int pageSize;
|
||||
|
||||
private long totalElements;
|
||||
private int totalPages;
|
||||
private boolean last;
|
||||
private PageMetadata page;
|
||||
|
||||
public PageResponse() {
|
||||
}
|
||||
@@ -63,10 +62,71 @@ public class PageResponse<T> {
|
||||
}
|
||||
|
||||
public boolean isLast() {
|
||||
return last;
|
||||
if (last) {
|
||||
return true;
|
||||
}
|
||||
if (page != null) {
|
||||
return page.number >= Math.max(0, page.totalPages - 1);
|
||||
}
|
||||
return content == null || content.isEmpty();
|
||||
}
|
||||
|
||||
public void setLast(boolean last) {
|
||||
this.last = last;
|
||||
}
|
||||
|
||||
public PageMetadata getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(PageMetadata page) {
|
||||
this.page = page;
|
||||
if (page != null) {
|
||||
this.pageNumber = page.number;
|
||||
this.pageSize = page.size;
|
||||
this.totalElements = page.totalElements;
|
||||
this.totalPages = page.totalPages;
|
||||
this.last = page.number >= Math.max(0, page.totalPages - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class PageMetadata {
|
||||
private int size;
|
||||
private int number;
|
||||
private long totalElements;
|
||||
private int totalPages;
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public int getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setNumber(int number) {
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public long getTotalElements() {
|
||||
return totalElements;
|
||||
}
|
||||
|
||||
public void setTotalElements(long totalElements) {
|
||||
this.totalElements = totalElements;
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
public void setTotalPages(int totalPages) {
|
||||
this.totalPages = totalPages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,39 @@ public class SaleApi {
|
||||
return pageResponse.getContent();
|
||||
}
|
||||
|
||||
public List<SaleResponse> listAllSales(String query) throws Exception {
|
||||
int page = 0;
|
||||
int size = 250;
|
||||
List<SaleResponse> allSales = new java.util.ArrayList<>();
|
||||
|
||||
while (true) {
|
||||
String path = "/api/v1/sales?page=" + page + "&size=" + size;
|
||||
if (query != null && !query.isEmpty()) {
|
||||
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
String response = apiClient.getRawResponse(path);
|
||||
PageResponse<SaleResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
||||
response,
|
||||
new TypeReference<PageResponse<SaleResponse>>() {}
|
||||
);
|
||||
if (pageResponse == null) {
|
||||
throw new IllegalStateException("Null response from sales endpoint");
|
||||
}
|
||||
|
||||
if (pageResponse.getContent() != null) {
|
||||
allSales.addAll(pageResponse.getContent());
|
||||
}
|
||||
|
||||
if (pageResponse.isLast()) {
|
||||
break;
|
||||
}
|
||||
page++;
|
||||
}
|
||||
|
||||
return allSales;
|
||||
}
|
||||
|
||||
public SaleResponse getSale(Long id) throws Exception {
|
||||
return apiClient.get("/api/v1/sales/" + id, SaleResponse.class);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
||||
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
||||
@@ -90,6 +94,7 @@ public class RefundDialogController {
|
||||
private final ObservableList<SaleItemResponse> originalItems = FXCollections.observableArrayList();
|
||||
private final ObservableList<RefundItem> refundItems = FXCollections.observableArrayList();
|
||||
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
||||
private boolean refundInProgress;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
@@ -100,17 +105,19 @@ public class RefundDialogController {
|
||||
}
|
||||
|
||||
private void setupTables() {
|
||||
colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
|
||||
colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||
colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
||||
colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal"));
|
||||
tvOriginalItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
colOriginalProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName()));
|
||||
colOriginalQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity()));
|
||||
colOriginalUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice()));
|
||||
colOriginalTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getLineTotal()));
|
||||
tvOriginalItems.setItems(originalItems);
|
||||
tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
|
||||
colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
|
||||
colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||
colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
||||
colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
|
||||
tvRefundItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
colRefundProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName()));
|
||||
colRefundQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity()));
|
||||
colRefundUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice()));
|
||||
colRefundTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getTotal()));
|
||||
tvRefundItems.setItems(refundItems);
|
||||
tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
}
|
||||
@@ -125,12 +132,13 @@ public class RefundDialogController {
|
||||
return;
|
||||
}
|
||||
txtSaleId.setText(String.valueOf(saleId));
|
||||
loadSale();
|
||||
Platform.runLater(this::loadSale);
|
||||
}
|
||||
|
||||
private void loadSale() {
|
||||
String saleIdText = txtSaleId.getText().trim();
|
||||
if (saleIdText.isEmpty()) {
|
||||
clearLoadedSale();
|
||||
showError("Load Sale", "Enter a transaction ID.");
|
||||
return;
|
||||
}
|
||||
@@ -139,22 +147,36 @@ public class RefundDialogController {
|
||||
try {
|
||||
saleId = Long.parseLong(saleIdText);
|
||||
} catch (NumberFormatException e) {
|
||||
clearLoadedSale();
|
||||
showError("Load Sale", "Invalid transaction ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<SaleResponse> allSales = SaleApi.getInstance().listSales(0, 1000, null);
|
||||
currentSale = SaleApi.getInstance().getSale(saleId);
|
||||
if (Boolean.TRUE.equals(currentSale.getIsRefund())) {
|
||||
clearLoadedSale();
|
||||
showError("Load Sale", "Select an original sale, not a refund record.");
|
||||
return;
|
||||
}
|
||||
List<SaleResponse> previousRefunds = allSales.stream()
|
||||
.filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId()))
|
||||
.collect(Collectors.toList());
|
||||
setLoadingState(true, "Loading sale...");
|
||||
clearLoadedSale();
|
||||
|
||||
Task<LoadedSaleData> task = new Task<>() {
|
||||
@Override
|
||||
protected LoadedSaleData call() throws Exception {
|
||||
List<SaleResponse> allSales = SaleApi.getInstance().listAllSales(null);
|
||||
SaleResponse sale = SaleApi.getInstance().getSale(saleId);
|
||||
if (Boolean.TRUE.equals(sale.getIsRefund())) {
|
||||
throw new IllegalStateException("Select an original sale, not a refund record.");
|
||||
}
|
||||
List<SaleResponse> previousRefunds = allSales.stream()
|
||||
.filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId()))
|
||||
.collect(Collectors.toList());
|
||||
List<SaleItemResponse> refundableItems = buildRefundableItems(sale, previousRefunds);
|
||||
if (refundableItems.isEmpty()) {
|
||||
throw new IllegalStateException("This sale has no remaining refundable items.");
|
||||
}
|
||||
return new LoadedSaleData(sale, refundableItems);
|
||||
}
|
||||
};
|
||||
|
||||
task.setOnSucceeded(event -> {
|
||||
LoadedSaleData loaded = task.getValue();
|
||||
currentSale = loaded.sale();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s",
|
||||
currentSale.getSaleDate().format(formatter),
|
||||
@@ -162,26 +184,25 @@ public class RefundDialogController {
|
||||
currency.format(currentSale.getTotalAmount()),
|
||||
currentSale.getPaymentMethod());
|
||||
lblSaleInfo.setText(saleInfo);
|
||||
|
||||
List<SaleItemResponse> refundableItems = buildRefundableItems(currentSale, previousRefunds);
|
||||
if (refundableItems.isEmpty()) {
|
||||
showError("Load Sale", "This sale has no remaining refundable items.");
|
||||
return;
|
||||
}
|
||||
|
||||
baseOriginalItems.clear();
|
||||
baseOriginalItems.addAll(copySaleItems(refundableItems));
|
||||
originalItems.setAll(copySaleItems(refundableItems));
|
||||
baseOriginalItems.addAll(copySaleItems(loaded.refundableItems()));
|
||||
originalItems.setAll(copySaleItems(loaded.refundableItems()));
|
||||
cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod());
|
||||
|
||||
refundItems.clear();
|
||||
updateOriginalItemAvailability();
|
||||
updateRefundTotal();
|
||||
setLoadingState(false, saleInfo);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale");
|
||||
task.setOnFailed(event -> {
|
||||
clearLoadedSale();
|
||||
Throwable e = task.getException();
|
||||
ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", (Exception) e, "Loading sale");
|
||||
setLoadingState(false, "");
|
||||
showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale.");
|
||||
}
|
||||
});
|
||||
|
||||
new Thread(task).start();
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -248,6 +269,9 @@ public class RefundDialogController {
|
||||
|
||||
@FXML
|
||||
void btnProcessRefundClicked(ActionEvent event) {
|
||||
if (refundInProgress) {
|
||||
return;
|
||||
}
|
||||
if (currentSale == null) {
|
||||
showError("Process Refund", "Load a sale first.");
|
||||
return;
|
||||
@@ -280,36 +304,53 @@ public class RefundDialogController {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SaleRequest request = new SaleRequest();
|
||||
request.setStoreId(storeId);
|
||||
request.setPaymentMethod(payment);
|
||||
request.setIsRefund(true);
|
||||
request.setOriginalSaleId(currentSale.getSaleId());
|
||||
SaleRequest request = new SaleRequest();
|
||||
request.setStoreId(storeId);
|
||||
request.setPaymentMethod(payment);
|
||||
request.setIsRefund(true);
|
||||
request.setOriginalSaleId(currentSale.getSaleId());
|
||||
|
||||
List<SaleItemRequest> items = new ArrayList<>();
|
||||
for (RefundItem item : refundItems) {
|
||||
SaleItemRequest saleItem = new SaleItemRequest();
|
||||
saleItem.setProdId((long) item.getProdId());
|
||||
saleItem.setQuantity(-item.getQuantity());
|
||||
items.add(saleItem);
|
||||
List<SaleItemRequest> items = new ArrayList<>();
|
||||
for (RefundItem item : refundItems) {
|
||||
SaleItemRequest saleItem = new SaleItemRequest();
|
||||
saleItem.setProdId((long) item.getProdId());
|
||||
saleItem.setQuantity(-item.getQuantity());
|
||||
items.add(saleItem);
|
||||
}
|
||||
request.setItems(items);
|
||||
|
||||
refundInProgress = true;
|
||||
setLoadingState(true, lblSaleInfo.getText());
|
||||
|
||||
Task<SaleResponse> task = new Task<>() {
|
||||
@Override
|
||||
protected SaleResponse call() throws Exception {
|
||||
return SaleApi.getInstance().createSale(request);
|
||||
}
|
||||
request.setItems(items);
|
||||
|
||||
SaleResponse refundResponse = SaleApi.getInstance().createSale(request);
|
||||
};
|
||||
|
||||
task.setOnSucceeded(evt -> {
|
||||
refundInProgress = false;
|
||||
setLoadingState(false, lblSaleInfo.getText());
|
||||
SaleResponse refundResponse = task.getValue();
|
||||
Alert success = new Alert(Alert.AlertType.INFORMATION);
|
||||
success.setTitle("Refund Processed");
|
||||
success.setHeaderText(null);
|
||||
success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully.");
|
||||
success.showAndWait();
|
||||
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund");
|
||||
showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund.");
|
||||
}
|
||||
task.setOnFailed(evt -> {
|
||||
refundInProgress = false;
|
||||
setLoadingState(false, lblSaleInfo.getText());
|
||||
Throwable e = task.getException();
|
||||
Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e);
|
||||
ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", ex, "Processing refund");
|
||||
showError("Process Refund", e != null && e.getMessage() != null ? e.getMessage() : "Could not process refund.");
|
||||
});
|
||||
|
||||
new Thread(task).start();
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -326,6 +367,18 @@ public class RefundDialogController {
|
||||
updateRefundTotal();
|
||||
}
|
||||
|
||||
private void setLoadingState(boolean loading, String message) {
|
||||
btnLoadSale.setDisable(loading);
|
||||
btnAddToRefund.setDisable(loading);
|
||||
btnRemoveFromRefund.setDisable(loading);
|
||||
btnProcessRefund.setDisable(loading);
|
||||
txtSaleId.setDisable(loading);
|
||||
cbPaymentMethod.setDisable(loading);
|
||||
if (loading) {
|
||||
lblSaleInfo.setText(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) {
|
||||
for (int i = 0; i < refundItems.size(); i++) {
|
||||
RefundItem existing = refundItems.get(i);
|
||||
@@ -486,4 +539,7 @@ public class RefundDialogController {
|
||||
return quantity * unitPrice;
|
||||
}
|
||||
}
|
||||
|
||||
private record LoadedSaleData(SaleResponse sale, List<SaleItemResponse> refundableItems) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user