- 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>
98 lines
2.8 KiB
C#
98 lines
2.8 KiB
C#
using FluentAssertions;
|
|
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
|
using OrderService.Domain.Exceptions;
|
|
using Xunit;
|
|
|
|
namespace OrderService.UnitTests.Domain;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for Order aggregate domain behavior.
|
|
/// VI: Unit tests cho hành vi domain của aggregate Order.
|
|
/// </summary>
|
|
public class OrderAggregateTests
|
|
{
|
|
[Fact]
|
|
public void CreateOrder_WithValidShopId_ShouldStartInDraftStatus()
|
|
{
|
|
// Arrange
|
|
var shopId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var order = new Order(shopId);
|
|
|
|
// Assert
|
|
order.Id.Should().NotBe(Guid.Empty);
|
|
order.ShopId.Should().Be(shopId);
|
|
order.Status.Should().Be(OrderStatus.Draft);
|
|
order.TotalAmount.Should().Be(0m);
|
|
order.DomainEvents.Should().NotBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void AddItem_InDraftStatus_ShouldRecalculateTotalAmount()
|
|
{
|
|
// Arrange
|
|
var order = new Order(Guid.NewGuid());
|
|
var firstItem = new OrderItem(Guid.NewGuid(), "Coffee", "PreparedFood", 2, 30_000m);
|
|
var secondItem = new OrderItem(Guid.NewGuid(), "Cake", "PreparedFood", 1, 45_000m);
|
|
|
|
// Act
|
|
order.AddItem(firstItem);
|
|
order.AddItem(secondItem);
|
|
|
|
// Assert
|
|
order.Items.Should().HaveCount(2);
|
|
order.TotalAmount.Should().Be(105_000m);
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsValidated_WithoutItems_ShouldThrowDomainException()
|
|
{
|
|
// Arrange
|
|
var order = new Order(Guid.NewGuid());
|
|
|
|
// Act
|
|
var act = () => order.MarkAsValidated();
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.WithMessage("Cannot validate order with no items");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullLifecycle_DraftToCompleted_ShouldUpdateStatusSequentially()
|
|
{
|
|
// Arrange
|
|
var order = new Order(Guid.NewGuid());
|
|
order.AddItem(new OrderItem(Guid.NewGuid(), "Shoes", "Physical", 1, 1_200_000m));
|
|
|
|
// Act
|
|
order.MarkAsValidated();
|
|
order.MarkAsPaid("cash", "TXN-TEST-001");
|
|
order.MarkAsProcessing();
|
|
order.MarkAsCompleted();
|
|
|
|
// Assert
|
|
order.Status.Should().Be(OrderStatus.Completed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_CompletedOrder_ShouldThrowDomainException()
|
|
{
|
|
// Arrange
|
|
var order = new Order(Guid.NewGuid());
|
|
order.AddItem(new OrderItem(Guid.NewGuid(), "Laptop", "Physical", 1, 25_000_000m));
|
|
order.MarkAsValidated();
|
|
order.MarkAsPaid("cash", "TXN-TEST-001");
|
|
order.MarkAsProcessing();
|
|
order.MarkAsCompleted();
|
|
|
|
// Act
|
|
var act = () => order.Cancel("Customer changed mind");
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.WithMessage("Cannot cancel completed order");
|
|
}
|
|
}
|