Backend (7 fixes):
- wallet-service: remove conflicting EF Ignore() calls for mapped backing fields
- fnb-engine: remove KitchenTicket short constructor that set productId=orderItemId
- fnb-engine: replace fire-and-forget Task.Run with direct await for inventory deduction
- TenantMiddleware: implement PostgreSQL RLS SET LOCAL in 4 services (wallet, fnb, inventory, catalog)
- order-service: fix SQL injection pattern in TenantMiddleware with Guid.ToString("D")
- order-service: add ValidateShopAccess() authorization check in SignalR PosHub
- 4 services: register IDbConnection (NpgsqlConnection) in DI for RLS middleware
Frontend (3 fixes):
- PosDataService: return Success=false (not true) when PayOrder response parsing fails
- QrPayment: add _disposed guard to prevent timer race condition after component disposal
- BFF OrderController: add [Authorize] attribute to require JWT for all endpoints
Infrastructure (3 fixes):
- docker-compose: upgrade PostgreSQL 15-alpine to 16-alpine per project spec
- init-databases.sh: add 4 missing marketing service databases (mkt_*)
- Traefik routes: add wallet, catalog, booking routers and /api/v1/stock path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
5.0 KiB
C#
170 lines
5.0 KiB
C#
using FluentAssertions;
|
|
using FnbEngine.Domain.AggregatesModel.KitchenAggregate;
|
|
using FnbEngine.Domain.Events;
|
|
using Xunit;
|
|
|
|
namespace FnbEngine.UnitTests.Domain;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for KitchenTicket aggregate behavior.
|
|
/// VI: Unit tests cho hanh vi aggregate KitchenTicket.
|
|
/// </summary>
|
|
public class KitchenTicketTests
|
|
{
|
|
private static readonly Guid ValidSessionId = Guid.NewGuid();
|
|
private static readonly Guid ValidOrderItemId = Guid.NewGuid();
|
|
private static readonly Guid ValidProductId = Guid.NewGuid();
|
|
|
|
[Fact]
|
|
public void Constructor_WithAllParams_ShouldCreatePendingTicket()
|
|
{
|
|
// Act
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo", 1, "Kitchen", 1);
|
|
|
|
// Assert
|
|
ticket.Id.Should().NotBeEmpty();
|
|
ticket.SessionId.Should().Be(ValidSessionId);
|
|
ticket.OrderItemId.Should().Be(ValidOrderItemId);
|
|
ticket.ProductId.Should().Be(ValidProductId);
|
|
ticket.ItemName.Should().Be("Pho Bo");
|
|
ticket.Station.Should().Be("Kitchen");
|
|
ticket.Priority.Should().Be(1);
|
|
ticket.Quantity.Should().Be(1);
|
|
ticket.Status.Should().Be("Pending");
|
|
ticket.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
|
ticket.CompletedAt.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithProductIdAndQuantity_ShouldSetCorrectValues()
|
|
{
|
|
// Act
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Bun Cha", 3, "Kitchen", 2);
|
|
|
|
// Assert
|
|
ticket.ProductId.Should().Be(ValidProductId);
|
|
ticket.Quantity.Should().Be(3);
|
|
ticket.ItemName.Should().Be("Bun Cha");
|
|
ticket.Priority.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithZeroQuantity_ShouldDefaultToOne()
|
|
{
|
|
// Act
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Item", 0);
|
|
|
|
// Assert
|
|
ticket.Quantity.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithNegativeQuantity_ShouldDefaultToOne()
|
|
{
|
|
// Act
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Item", -5);
|
|
|
|
// Assert
|
|
ticket.Quantity.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithNullStation_ShouldAllowNull()
|
|
{
|
|
// Act
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Drink");
|
|
|
|
// Assert
|
|
ticket.Station.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsInProgress_ShouldChangeStatusToInProgress()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act
|
|
ticket.MarkAsInProgress();
|
|
|
|
// Assert
|
|
ticket.Status.Should().Be("InProgress");
|
|
ticket.CompletedAt.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsReady_ShouldChangeStatusAndSetCompletedAt()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act
|
|
ticket.MarkAsReady();
|
|
|
|
// Assert
|
|
ticket.Status.Should().Be("Ready");
|
|
ticket.CompletedAt.Should().NotBeNull();
|
|
ticket.CompletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsServed_ShouldChangeStatusToServed()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act
|
|
ticket.MarkAsServed();
|
|
|
|
// Assert
|
|
ticket.Status.Should().Be("Served");
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsServed_ShouldRaiseKitchenTicketServedDomainEvent()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act
|
|
ticket.MarkAsServed();
|
|
|
|
// Assert
|
|
ticket.DomainEvents.Should().ContainSingle();
|
|
ticket.DomainEvents.Should().ContainSingle(e => e is KitchenTicketServedDomainEvent);
|
|
var domainEvent = ticket.DomainEvents.OfType<KitchenTicketServedDomainEvent>().Single();
|
|
domainEvent.Ticket.Should().BeSameAs(ticket);
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkAsServed_CalledTwice_ShouldRaiseTwoDomainEvents()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act
|
|
ticket.MarkAsServed();
|
|
ticket.MarkAsServed();
|
|
|
|
// Assert
|
|
ticket.DomainEvents.Should().HaveCount(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void StatusTransition_PendingToInProgressToReadyToServed_ShouldSucceed()
|
|
{
|
|
// Arrange
|
|
var ticket = new KitchenTicket(ValidSessionId, ValidOrderItemId, ValidProductId, "Pho Bo");
|
|
|
|
// Act & Assert
|
|
ticket.MarkAsInProgress();
|
|
ticket.Status.Should().Be("InProgress");
|
|
|
|
ticket.MarkAsReady();
|
|
ticket.Status.Should().Be("Ready");
|
|
|
|
ticket.MarkAsServed();
|
|
ticket.Status.Should().Be("Served");
|
|
}
|
|
}
|