fix(pos): prevent duplicate orders by checking payment API result
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) <noreply@anthropic.com>
This commit is contained in:
@@ -209,8 +209,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment" disabled="@(_receivedAmount < FinalTotal)">
|
||||
Xác nhận thanh toán
|
||||
@if (!string.IsNullOrEmpty(_paymentError))
|
||||
{
|
||||
<div style="font-size:12px;color:var(--pos-danger);margin-bottom:8px;text-align:center;">@_paymentError</div>
|
||||
}
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment" disabled="@(_receivedAmount < FinalTotal || _paymentProcessing)">
|
||||
@if (_paymentProcessing)
|
||||
{
|
||||
<span>Đang xử lý...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Xác nhận thanh toán</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,8 +257,19 @@
|
||||
}
|
||||
</div>
|
||||
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment">
|
||||
Xác nhận đã thanh toán
|
||||
@if (!string.IsNullOrEmpty(_paymentError))
|
||||
{
|
||||
<div style="font-size:12px;color:var(--pos-danger);margin-bottom:8px;text-align:center;">@_paymentError</div>
|
||||
}
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment" disabled="@_paymentProcessing">
|
||||
@if (_paymentProcessing)
|
||||
{
|
||||
<span>Đang xử lý...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Xác nhận đã thanh toán</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user