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 b4177062..ee6cd0dc 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 @@ -435,6 +435,7 @@ // ═══ SENT ITEMS (per table — items sent to kitchen, not yet paid) ═══ private readonly Dictionary> _tableOrders = new(); + private readonly Dictionary> _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); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index 00d9d711..b8cd6a15 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -627,6 +627,15 @@ public class PosDataService return null; } + // ═══ PAY ORDER ═══ + + public async Task 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) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs index 2ea4716c..e767ddcd 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs @@ -113,6 +113,17 @@ public class OrderController : ControllerBase new { reason = "Cancelled from POS" }).ProxyAsync(); } + /// + /// EN: Pay an order. + /// VI: Thanh toán đơn hàng. + /// + [HttpPost("orders/{orderId:guid}/pay")] + public Task PayOrder(Guid orderId, [FromQuery] Guid? shopId = null) + { + var qs = shopId.HasValue ? $"?shopId={shopId}" : ""; + return _order.PostAsJsonAsync($"/api/v1/orders/{orderId}/pay{qs}", new { }).ProxyAsync(); + } + /// /// 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. diff --git a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs index bf44318c..c0e33a9c 100644 --- a/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/services/order-service-net/src/OrderService.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -46,10 +46,10 @@ public class Order : Entity, IAggregateRoot public Guid? TableId => _tableId; /// - /// 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). /// - public OrderStatus Status => _status; + public OrderStatus Status => _status ?? Enumeration.FromValue(StatusId); /// /// EN: Status ID for EF Core mapping. @@ -134,8 +134,8 @@ public class Order : Entity, IAggregateRoot /// 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 /// 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 /// 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 /// 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 /// 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;