- 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>
218 lines
9.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|