Files
pos-system/services/order-service-net/src/OrderService.API/Application/Commands/CreateOrderCommandHandler.cs
Ho Ngoc Hai 8af86e9e89 feat: implement Phase 1 payment gateway, real-time SignalR, kitchen-inventory deduction, and order payment flow
- wallet-service: IPaymentGateway abstraction + VN Pay implementation (HMAC-SHA512, sandbox), Payment aggregate root, PaymentsController with create/callback/query endpoints
- order-service: PosHub SignalR hub with Redis backplane + MessagePack, strongly-typed clients, 3 group types (shop/kds/pos), integrated into Create/Pay/Complete/Cancel order handlers
- fnb-engine + inventory-service: Kitchen→Inventory auto-deduction via domain events, HTTP with Polly retry + circuit breaker, idempotency check, graceful degradation on insufficient stock
- order-service: Enhanced PayOrderCommand with 3 flows (cash/card/online), PaymentPending status, WalletServiceClient, CompleteOrderPaymentCommand for gateway callbacks
- POS frontend: Cash/Card/QR payment components wired to real backend, BFF proxy updated
- infra: Traefik routes for fnb-engine, inventory-service, and SignalR WebSocket hub
- ROADMAP.md: Updated with Phase 1 progress tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:28:46 +07:00

158 lines
6.1 KiB
C#

// EN: Handler for CreateOrderCommand.
// VI: Handler cho CreateOrderCommand.
using MediatR;
using OrderService.API.Hubs;
using OrderService.Domain.AggregatesModel.OrderAggregate;
using OrderService.Domain.Strategies;
namespace OrderService.API.Application.Commands;
/// <summary>
/// EN: Handler for creating a new order with strategy-based validation.
/// VI: Handler tạo order mới với validation qua strategy.
/// </summary>
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
private readonly IOrderRepository _orderRepository;
private readonly IEnumerable<ILineItemStrategy> _strategies;
private readonly IPosNotificationService _posNotificationService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
IEnumerable<ILineItemStrategy> strategies,
IPosNotificationService posNotificationService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_strategies = strategies ?? throw new ArgumentNullException(nameof(strategies));
_posNotificationService = posNotificationService ?? throw new ArgumentNullException(nameof(posNotificationService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<CreateOrderResult> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"EN: Creating order for shop {ShopId} / VI: Tạo order cho shop {ShopId}",
request.ShopId);
// EN: Create order aggregate
// VI: Tạo order aggregate
var order = new Order(request.ShopId, request.CustomerId, request.TableId);
// EN: Add items to order
// VI: Thêm items vào order
foreach (var itemRequest in request.Items)
{
var orderItem = new OrderItem(
itemRequest.ProductId,
itemRequest.ProductName,
itemRequest.ProductType,
itemRequest.Quantity,
itemRequest.UnitPrice,
trackInventory: itemRequest.TrackInventory);
order.AddItem(orderItem);
}
// EN: Validate all items through their strategies
// VI: Validate tất cả items qua strategies của chúng
foreach (var item in order.Items)
{
var strategy = GetStrategy(item.ProductType);
var isValid = await strategy.ValidateAsync(item, request.ShopId, cancellationToken);
if (!isValid)
{
throw new InvalidOperationException(
$"Validation failed for item {item.ProductName} (type: {item.ProductType})");
}
}
// EN: Execute all items via their strategies (e.g., create kitchen tickets for PreparedFood)
// VI: Thực thi tất cả items qua strategies (vd: tạo phiếu bếp cho PreparedFood)
foreach (var item in order.Items)
{
var strategy = GetStrategy(item.ProductType);
await strategy.ExecuteAsync(item, request.ShopId, cancellationToken);
}
// EN: Apply discount if provided
// VI: Áp dụng giảm giá nếu có
if (request.DiscountAmount is > 0)
{
order.ApplyDiscount(request.DiscountAmount.Value, request.DiscountType, request.DiscountReference);
}
// EN: Mark order as validated after all items pass validation
// VI: Đánh dấu order là validated sau khi tất cả items pass validation
order.MarkAsValidated();
// EN: Save order
// VI: Lưu order
_orderRepository.Add(order);
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"EN: Order created successfully / VI: Tạo order thành công: {OrderId}",
order.Id);
// EN: Send real-time notification to POS/KDS clients
// VI: Gửi thông báo real-time đến POS/KDS clients
try
{
var orderNotification = new OrderNotificationDto(
OrderId: order.Id,
ShopId: order.ShopId,
Status: order.Status.Name,
Items: order.Items.Select(i => new OrderItemNotificationDto(
ItemId: i.Id,
ProductId: i.ProductId,
ProductName: i.ProductName,
ProductType: i.ProductType,
Quantity: i.Quantity,
UnitPrice: i.UnitPrice,
TotalPrice: i.TotalPrice,
Status: i.Status
)).ToList().AsReadOnly(),
TotalAmount: order.TotalAmount,
CustomerId: order.CustomerId,
TableId: order.TableId,
CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt
);
await _posNotificationService.NotifyOrderCreatedAsync(
order.ShopId, orderNotification, cancellationToken);
}
catch (Exception ex)
{
// EN: Don't fail the command if notification fails
// VI: Không fail command nếu notification thất bại
_logger.LogWarning(ex,
"EN: Failed to send POS notification for order {OrderId} / " +
"VI: Gửi thông báo POS thất bại cho order {OrderId}",
order.Id);
}
return new CreateOrderResult(
order.Id,
order.TotalAmount,
order.Status.Name);
}
private ILineItemStrategy GetStrategy(string productType)
{
var strategy = _strategies.FirstOrDefault(s => s.SupportedType == productType);
if (strategy == null)
{
throw new InvalidOperationException(
$"EN: No strategy found for product type / VI: Không tìm thấy strategy cho loại sản phẩm: {productType}");
}
return strategy;
}
}