feat(pos): implement order payment flow and update order aggregate status handling.

This commit is contained in:
Ho Ngoc Hai
2026-03-05 08:05:19 +07:00
parent 0901e91673
commit cfcdbd069d
4 changed files with 46 additions and 14 deletions

View File

@@ -435,6 +435,7 @@
// ═══ SENT ITEMS (per table — items sent to kitchen, not yet paid) ═══
private readonly Dictionary<string, List<SentItem>> _tableOrders = new();
private readonly Dictionary<string, List<Guid>> _tableOrderIds = new();
private bool _sendingToKitchen;
private string? _kitchenMessage;
private bool _kitchenSuccess;
@@ -684,10 +685,17 @@
_lastReceiptItems = GetSentItemsForTable(SelectedTable).ToList();
_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))
{
foreach (var orderId in orderIds)
await DataService.PayOrderAsync(orderId, ShopId);
}
_paymentProcessing = false;
_paymentStep = PayStep.Success;
// Reload table orders from DB (the paid order will no longer be Validated)
// Reload table orders from DB (paid orders no longer Validated → removed)
await LoadTableOrdersFromDb();
await SaveStateToLocalStorage();
StateHasChanged();
@@ -745,12 +753,16 @@
{
var activeOrders = await DataService.GetActiveTableOrdersAsync(ShopId);
_tableOrders.Clear();
_tableOrderIds.Clear();
foreach (var order in activeOrders)
{
if (order.TableId == null) continue;
var tableKey = order.TableId.Value.ToString();
if (!_tableOrders.ContainsKey(tableKey))
_tableOrders[tableKey] = new();
if (!_tableOrderIds.ContainsKey(tableKey))
_tableOrderIds[tableKey] = new();
_tableOrderIds[tableKey].Add(order.OrderId);
foreach (var item in order.Items)
{
var existing = _tableOrders[tableKey].FirstOrDefault(s => s.ProductId == item.ProductId);

View File

@@ -627,6 +627,15 @@ public class PosDataService
return null;
}
// ═══ PAY ORDER ═══
public async Task<bool> PayOrderAsync(Guid orderId, Guid shopId)
{
AttachToken();
var resp = await _http.PostAsJsonAsync($"api/bff/orders/{orderId}/pay?shopId={shopId}", new { }, _writeOptions);
return resp.IsSuccessStatusCode;
}
// ═══ ACTIVE TABLE ORDERS ═══
// EN: DTOs for active table orders (orders with table_id, status=Validated)

View File

@@ -113,6 +113,17 @@ public class OrderController : ControllerBase
new { reason = "Cancelled from POS" }).ProxyAsync();
}
/// <summary>
/// EN: Pay an order.
/// VI: Thanh toán đơn hàng.
/// </summary>
[HttpPost("orders/{orderId:guid}/pay")]
public Task<IActionResult> PayOrder(Guid orderId, [FromQuery] Guid? shopId = null)
{
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
return _order.PostAsJsonAsync($"/api/v1/orders/{orderId}/pay{qs}", new { }).ProxyAsync();
}
/// <summary>
/// EN: Create a POS order. Enriches items with correct productType based on shop category.
/// VI: Tạo đơn POS. Bổ sung productType chính xác cho items dựa trên loại shop.

View File

@@ -46,10 +46,10 @@ public class Order : Entity, IAggregateRoot
public Guid? TableId => _tableId;
/// <summary>
/// EN: Order status.
/// VI: Trạng thái đơn hàng.
/// EN: Order status (resolved from StatusId when loaded from DB).
/// VI: Trạng thái đơn hàng (resolve từ StatusId khi load từ DB).
/// </summary>
public OrderStatus Status => _status;
public OrderStatus Status => _status ?? Enumeration.FromValue<OrderStatus>(StatusId);
/// <summary>
/// EN: Status ID for EF Core mapping.
@@ -134,8 +134,8 @@ public class Order : Entity, IAggregateRoot
/// </summary>
public void MarkAsValidated()
{
if (_status != OrderStatus.Draft)
throw new DomainException($"Cannot validate order with status {_status.Name}");
if (Status != OrderStatus.Draft)
throw new DomainException($"Cannot validate order with status {Status.Name}");
if (!_items.Any())
throw new DomainException("Cannot validate order with no items");
@@ -150,8 +150,8 @@ public class Order : Entity, IAggregateRoot
/// </summary>
public void MarkAsPaid()
{
if (_status != OrderStatus.Validated)
throw new DomainException($"Cannot mark as paid order with status {_status.Name}");
if (Status != OrderStatus.Validated)
throw new DomainException($"Cannot mark as paid order with status {Status.Name}");
_status = OrderStatus.Paid;
StatusId = OrderStatus.Paid.Id;
@@ -166,8 +166,8 @@ public class Order : Entity, IAggregateRoot
/// </summary>
public void MarkAsProcessing()
{
if (_status != OrderStatus.Paid)
throw new DomainException($"Cannot process order with status {_status.Name}");
if (Status != OrderStatus.Paid)
throw new DomainException($"Cannot process order with status {Status.Name}");
_status = OrderStatus.Processing;
StatusId = OrderStatus.Processing.Id;
@@ -180,8 +180,8 @@ public class Order : Entity, IAggregateRoot
/// </summary>
public void MarkAsCompleted()
{
if (_status != OrderStatus.Processing)
throw new DomainException($"Cannot complete order with status {_status.Name}");
if (Status != OrderStatus.Processing)
throw new DomainException($"Cannot complete order with status {Status.Name}");
_status = OrderStatus.Completed;
StatusId = OrderStatus.Completed.Id;
@@ -196,9 +196,9 @@ public class Order : Entity, IAggregateRoot
/// </summary>
public void Cancel(string reason)
{
if (_status == OrderStatus.Completed)
if (Status == OrderStatus.Completed)
throw new DomainException("Cannot cancel completed order");
if (_status == OrderStatus.Cancelled)
if (Status == OrderStatus.Cancelled)
throw new DomainException("Order is already cancelled");
_status = OrderStatus.Cancelled;