From b81c6ac176a18ad3c47f2be8c4f31c2f980dd8f7 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 29 Mar 2026 02:12:57 +0700 Subject: [PATCH] fix(pos): prevent duplicate orders by checking payment API result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConfirmPayment() previously ignored PayOrderAsync return value and always showed success screen, even when payment API returned 500. This caused users to unknowingly create duplicate orders — one unpaid (Validated) and one paid (Completed). Root causes fixed: - Pass amountTendered to PayOrderWithDetailsAsync (required by PayOrderCommandValidator for cash payments) - Check payment API response before showing success screen - Show error message with retry option when payment fails - Add loading state to prevent double-clicks during processing Affects: CafeDesktop, CafeMobile, CafeTablet, RestaurantDesktop Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Pages/Pos/Cafe/CafeDesktop.razor | 57 +++++++++++++++---- .../Pages/Pos/Cafe/CafeMobile.razor | 23 ++++++-- .../Pages/Pos/Cafe/CafeTablet.razor | 23 ++++++-- .../Pos/Restaurant/RestaurantDesktop.razor | 33 ++++++++++- 4 files changed, 112 insertions(+), 24 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor index ce967c37..2fb4f001 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor @@ -209,8 +209,19 @@
-
@@ -246,8 +257,19 @@ }
-
@@ -932,11 +954,13 @@ } private bool _paymentProcessing; + private string? _paymentError; private async Task ConfirmPayment() { if (_paymentProcessing) return; _paymentProcessing = true; + _paymentError = null; StateHasChanged(); _lastOrderTotal = FinalTotal; @@ -946,16 +970,26 @@ // EN: Call API to mark order as paid (order was already created in StartPayment) // VI: Gọi API đánh dấu đơn đã thanh toán (đơn đã được tạo ở StartPayment) + bool paymentSuccess = false; try { if (_createdOrderId.HasValue) { - await DataService.PayOrderAsync(_createdOrderId.Value, ShopId, _selectedMethod); + // EN: Pass amountTendered for cash (required by validator), use FinalTotal for non-cash + // VI: Truyền amountTendered cho tiền mặt (validator yêu cầu), dùng FinalTotal cho phương thức khác + var amount = _selectedMethod == "cash" ? _receivedAmount : FinalTotal; + var result = await DataService.PayOrderWithDetailsAsync( + _createdOrderId.Value, ShopId, _selectedMethod, amount); + paymentSuccess = result?.Success ?? false; _lastTransactionId = _createdOrderId.Value.ToString()[..8].ToUpper(); } - else + + if (!paymentSuccess) { - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = "Thanh toán thất bại. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } // EN: Redeem voucher if applied / VI: Sử dụng voucher nếu đã áp dụng @@ -964,11 +998,12 @@ try { await DataService.RedeemVoucherAsync(_appliedVoucher.VoucherId.Value, _discountAmount); } catch { } } } - catch + catch (Exception ex) { - // EN: Fallback — generate local ID if API fails - // VI: Fallback — tạo ID local nếu API lỗi - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = $"Lỗi thanh toán: {ex.Message}. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } // EN: Save to session history (in-memory for this POS session) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeMobile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeMobile.razor index 49beed38..57ea4fd8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeMobile.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeMobile.razor @@ -518,27 +518,37 @@ } private bool _paymentProcessing; + private string? _paymentError; private async Task ConfirmPayment() { if (_paymentProcessing) return; _paymentProcessing = true; + _paymentError = null; StateHasChanged(); _lastOrderTotal = FinalTotal; _lastPaymentMethod = _selectedMethod; _lastReceiptItems = _cartItems.Select(i => (i.Name, i.Qty, i.Price)).ToList(); + bool paymentSuccess = false; try { if (_createdOrderId.HasValue) { - await DataService.PayOrderAsync(_createdOrderId.Value, ShopId); + var amount = _selectedMethod == "cash" ? _receivedAmount : FinalTotal; + var result = await DataService.PayOrderWithDetailsAsync( + _createdOrderId.Value, ShopId, _selectedMethod, amount); + paymentSuccess = result?.Success ?? false; _lastTransactionId = _createdOrderId.Value.ToString()[..8].ToUpper(); } - else + + if (!paymentSuccess) { - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = "Thanh toán thất bại. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } if (_appliedVoucher?.VoucherId != null && _discountAmount > 0) @@ -546,9 +556,12 @@ try { await DataService.RedeemVoucherAsync(_appliedVoucher.VoucherId.Value, _discountAmount); } catch { } } } - catch + catch (Exception ex) { - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = $"Lỗi thanh toán: {ex.Message}. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } _paymentProcessing = false; diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeTablet.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeTablet.razor index 23eb0331..4eb47a59 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeTablet.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeTablet.razor @@ -495,27 +495,37 @@ } private bool _paymentProcessing; + private string? _paymentError; private async Task ConfirmPayment() { if (_paymentProcessing) return; _paymentProcessing = true; + _paymentError = null; StateHasChanged(); _lastOrderTotal = FinalTotal; _lastPaymentMethod = _selectedMethod; _lastReceiptItems = _cartItems.Select(i => (i.Name, i.Qty, i.Price)).ToList(); + bool paymentSuccess = false; try { if (_createdOrderId.HasValue) { - await DataService.PayOrderAsync(_createdOrderId.Value, ShopId); + var amount = _selectedMethod == "cash" ? _receivedAmount : FinalTotal; + var result = await DataService.PayOrderWithDetailsAsync( + _createdOrderId.Value, ShopId, _selectedMethod, amount); + paymentSuccess = result?.Success ?? false; _lastTransactionId = _createdOrderId.Value.ToString()[..8].ToUpper(); } - else + + if (!paymentSuccess) { - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = "Thanh toán thất bại. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } if (_appliedVoucher?.VoucherId != null && _discountAmount > 0) @@ -523,9 +533,12 @@ try { await DataService.RedeemVoucherAsync(_appliedVoucher.VoucherId.Value, _discountAmount); } catch { } } } - catch + catch (Exception ex) { - _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; + _paymentError = $"Lỗi thanh toán: {ex.Message}. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } _paymentProcessing = false; diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantDesktop.razor index ee6cd0dc..cc879a36 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantDesktop.razor @@ -674,10 +674,13 @@ _receivedAmount = val; } + private string? _paymentError; + private async Task ConfirmPayment() { if (_paymentProcessing || SelectedTable == null) return; _paymentProcessing = true; + _paymentError = null; StateHasChanged(); _lastOrderTotal = GrandTotal; @@ -686,10 +689,34 @@ _lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}"; // Pay all orders for this table in the backend - if (_tableOrderIds.TryGetValue(SelectedTable.Id, out var orderIds)) + bool allPaid = true; + try { - foreach (var orderId in orderIds) - await DataService.PayOrderAsync(orderId, ShopId); + if (_tableOrderIds.TryGetValue(SelectedTable.Id, out var orderIds)) + { + var amount = _selectedMethod == "cash" ? _receivedAmount : GrandTotal; + foreach (var orderId in orderIds) + { + var result = await DataService.PayOrderWithDetailsAsync( + orderId, ShopId, _selectedMethod, amount); + if (result?.Success != true) allPaid = false; + } + } + + if (!allPaid) + { + _paymentError = "Thanh toán thất bại. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; + } + } + catch (Exception ex) + { + _paymentError = $"Lỗi thanh toán: {ex.Message}. Vui lòng thử lại."; + _paymentProcessing = false; + StateHasChanged(); + return; } _paymentProcessing = false;