- 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>
158 lines
6.1 KiB
C#
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;
|
|
}
|
|
}
|