Files
pos-system/services/order-service-net/src/OrderService.API/Application/Commands/PayOrderCommandHandler.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

218 lines
9.4 KiB
C#

// EN: Handler for PayOrderCommand — routes to cash, card, or online payment flow.
// VI: Handler cho PayOrderCommand — điều hướng đến thanh toán tiền mặt, thẻ, hoặc trực tuyến.
using MediatR;
using OrderService.API.Hubs;
using OrderService.Domain.AggregatesModel.OrderAggregate;
using OrderService.Domain.Exceptions;
using OrderService.Infrastructure.ExternalServices;
namespace OrderService.API.Application.Commands;
/// <summary>
/// EN: Handler for processing payment based on payment method.
/// VI: Handler xử lý thanh toán dựa trên phương thức thanh toán.
/// </summary>
public class PayOrderCommandHandler : IRequestHandler<PayOrderCommand, PayOrderResult>
{
private readonly IOrderRepository _orderRepository;
private readonly IWalletServiceClient _walletServiceClient;
private readonly IPosNotificationService _posNotificationService;
private readonly ILogger<PayOrderCommandHandler> _logger;
public PayOrderCommandHandler(
IOrderRepository orderRepository,
IWalletServiceClient walletServiceClient,
IPosNotificationService posNotificationService,
ILogger<PayOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_walletServiceClient = walletServiceClient ?? throw new ArgumentNullException(nameof(walletServiceClient));
_posNotificationService = posNotificationService ?? throw new ArgumentNullException(nameof(posNotificationService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PayOrderResult> Handle(
PayOrderCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"EN: Processing {PaymentMethod} payment for order {OrderId} / VI: Xử lý thanh toán {PaymentMethod} cho order {OrderId}",
request.PaymentMethod, request.OrderId);
// EN: Load order / VI: Load order
var order = await _orderRepository.GetByIdAsync(request.OrderId, cancellationToken);
if (order == null)
{
throw new DomainException($"Order not found: {request.OrderId}");
}
// EN: Verify shop ownership / VI: Xác minh quyền sở hữu shop
if (order.ShopId != request.ShopId)
{
throw new DomainException("Order does not belong to this shop");
}
var method = request.PaymentMethod.ToLowerInvariant();
return method switch
{
"cash" => await ProcessCashPayment(order, request, cancellationToken),
"card" => await ProcessCardPayment(order, request, cancellationToken),
"vnpay" or "momo" => await ProcessOnlinePayment(order, request, method, cancellationToken),
// EN: Default — treat "qr" and "transfer" as immediate like card (POS-confirmed)
// VI: Mặc định — xử lý "qr" và "transfer" như thẻ (POS xác nhận)
_ => await ProcessCardPayment(order, request, cancellationToken),
};
}
/// <summary>
/// EN: Process cash payment — instant, calculate change.
/// VI: Xử lý thanh toán tiền mặt — ngay lập tức, tính tiền thối.
/// </summary>
private async Task<PayOrderResult> ProcessCashPayment(
Order order,
PayOrderCommand request,
CancellationToken cancellationToken)
{
var amountTendered = request.AmountTendered ?? order.TotalAmount;
if (amountTendered < order.TotalAmount)
{
return new PayOrderResult(
false, order.Status.Name, null, null, null,
$"Insufficient cash: tendered {amountTendered}, required {order.TotalAmount}");
}
var transactionId = $"CASH-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..8].ToUpper()}";
var changeAmount = amountTendered - order.TotalAmount;
// EN: Mark as paid and processing / VI: Đánh dấu đã thanh toán và đang xử lý
order.MarkAsPaid("cash", transactionId, amountTendered);
order.MarkAsProcessing();
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"EN: Cash payment completed for order {OrderId}, change: {Change} / VI: Thanh toán tiền mặt hoàn tất cho order {OrderId}, tiền thối: {Change}",
order.Id, changeAmount);
// EN: Send real-time payment notification / VI: Gửi thông báo thanh toán real-time
await SendPaymentNotificationAsync(order, "cash", cancellationToken);
return new PayOrderResult(true, order.Status.Name, null, changeAmount, transactionId, null);
}
/// <summary>
/// EN: Process card payment — instant (terminal handles actual charge).
/// VI: Xử lý thanh toán thẻ — ngay lập tức (terminal xử lý giao dịch thực).
/// </summary>
private async Task<PayOrderResult> ProcessCardPayment(
Order order,
PayOrderCommand request,
CancellationToken cancellationToken)
{
var transactionId = $"CARD-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..8].ToUpper()}";
// EN: Mark as paid and processing / VI: Đánh dấu đã thanh toán và đang xử lý
order.MarkAsPaid(request.PaymentMethod.ToLowerInvariant(), transactionId);
order.MarkAsProcessing();
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"EN: Card payment completed for order {OrderId} / VI: Thanh toán thẻ hoàn tất cho order {OrderId}",
order.Id);
// EN: Send real-time payment notification / VI: Gửi thông báo thanh toán real-time
await SendPaymentNotificationAsync(order, request.PaymentMethod.ToLowerInvariant(), cancellationToken);
return new PayOrderResult(true, order.Status.Name, null, null, transactionId, null);
}
/// <summary>
/// EN: Process online payment (VNPay/Momo) — creates payment via wallet-service, returns redirect URL.
/// VI: Xử lý thanh toán trực tuyến (VNPay/Momo) — tạo payment qua wallet-service, trả về URL redirect.
/// </summary>
private async Task<PayOrderResult> ProcessOnlinePayment(
Order order,
PayOrderCommand request,
string gateway,
CancellationToken cancellationToken)
{
try
{
var paymentResponse = await _walletServiceClient.CreatePaymentAsync(
order.Id,
order.TotalAmount,
gateway,
request.ReturnUrl ?? "",
request.IpAddress ?? "127.0.0.1",
cancellationToken);
if (paymentResponse == null || string.IsNullOrEmpty(paymentResponse.PaymentUrl))
{
return new PayOrderResult(
false, order.Status.Name, null, null, null,
"Failed to create online payment / Không thể tạo thanh toán trực tuyến");
}
// EN: Mark order as payment pending / VI: Đánh dấu order chờ thanh toán
order.MarkAsPaymentPending(gateway, paymentResponse.TransactionId);
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"EN: Online payment initiated for order {OrderId} via {Gateway}, txn: {TxnId} / VI: Thanh toán trực tuyến đã khởi tạo cho order {OrderId} qua {Gateway}, giao dịch: {TxnId}",
order.Id, gateway, paymentResponse.TransactionId);
return new PayOrderResult(
true, order.Status.Name, paymentResponse.PaymentUrl, null, paymentResponse.TransactionId, null);
}
catch (Exception ex)
{
_logger.LogError(ex,
"EN: Failed to create online payment for order {OrderId} / VI: Không thể tạo thanh toán trực tuyến cho order {OrderId}",
order.Id);
return new PayOrderResult(
false, order.Status.Name, null, null, null,
$"Payment gateway error: {ex.Message}");
}
}
/// <summary>
/// EN: Send real-time payment notification to POS/KDS clients.
/// VI: Gửi thông báo thanh toán real-time đến POS/KDS clients.
/// </summary>
private async Task SendPaymentNotificationAsync(
Order order,
string paymentMethod,
CancellationToken cancellationToken)
{
try
{
var payment = new PaymentNotificationDto(
OrderId: order.Id,
Amount: order.TotalAmount,
Method: paymentMethod,
Status: "Completed");
await _posNotificationService.NotifyPaymentCompletedAsync(
order.ShopId, payment, cancellationToken);
await _posNotificationService.NotifyOrderStatusChangedAsync(
order.ShopId, order.Id, "Validated", order.Status.Name, 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 payment notification for order {OrderId} / " +
"VI: Gửi thông báo thanh toán thất bại cho order {OrderId}",
order.Id);
}
}
}