Files
pos-system/services/order-service-net/tests/OrderService.UnitTests/Domain/OrderAggregateTests.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

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");
}
}