using Meezi.API.Models.Orders; using Meezi.API.Services; using LiveOrderDto = Meezi.API.Models.Orders.LiveOrderDto; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Xunit; namespace Meezi.API.Tests; public class OrderSessionTests { private sealed class NullKdsNotifier : IKdsNotifier { public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => Task.CompletedTask; } private static (AppDbContext Db, OrderService Orders, TableService Tables, string CafeId, string TableId, string MenuItemId) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-1"; var branchId = "branch-1"; var tableId = "table-1"; var menuItemId = "item-1"; var categoryId = "cat-1"; var userId = "user-1"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = PlanTier.Pro }); db.Branches.Add(new Branch { Id = branchId, CafeId = cafeId, Name = "Main", IsActive = true, UpdatedAt = DateTime.UtcNow }); db.Employees.Add(new Employee { Id = userId, CafeId = cafeId, BranchId = branchId, Name = "Cashier", Phone = "09121111111", Role = EmployeeRole.Cashier }); db.Tables.Add(new Table { Id = tableId, CafeId = cafeId, BranchId = branchId, Number = "5", QrCode = "qr1", Capacity = 4 }); db.MenuCategories.Add(new MenuCategory { Id = categoryId, CafeId = cafeId, Name = "Drinks", NameEn = "Drinks", SortOrder = 0 }); db.MenuItems.Add(new MenuItem { Id = menuItemId, CafeId = cafeId, CategoryId = categoryId, Name = "Espresso", NameEn = "Espresso", Price = 100_000m, IsAvailable = true }); db.SaveChanges(); var shifts = new ShiftService(db); shifts.OpenShiftAsync(cafeId, branchId, 0m, userId).GetAwaiter().GetResult(); var kds = new NoOpKdsNotifier(); var snapp = new NoOpSnappfood(); var orders = new OrderService(db, kds, snapp, new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); var tables = new TableService( db, new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(), new NullKdsNotifier(), new BranchIdentityService(db)); return (db, orders, tables, cafeId, tableId, menuItemId); } private static CreateOrderRequest DineInRequest(string tableId, string menuItemId, int qty = 1) => new( OrderType.DineIn, "branch-1", tableId, null, "Ali", "09121111111", null, null, [new CreateOrderItemRequest(menuItemId, qty, null)]); [Fact] public async Task CreateOrder_SameTable_MergesIntoSingleOpenOrder() { var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); var tenant = new TestTenant(cafeId, "user-1"); var first = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); var second = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId, 2), default); Assert.True(first.Success); Assert.True(second.Success); Assert.Equal(first.Data!.Id, second.Data!.Id); Assert.Equal(3, second.Data!.Items.Sum(i => i.Quantity)); } [Fact] public async Task AppendItems_IncreasesTotal() { var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); var tenant = new TestTenant(cafeId, "user-1"); var created = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); var append = await orders.AppendOrderItemsAsync( cafeId, created.Data!.Id, new AppendOrderItemsRequest([new CreateOrderItemRequest(menuItemId, 1, null)]), default); Assert.True(append.Success); Assert.Equal(200_000m, append.Data!.Subtotal); } [Fact] public async Task GetOpenOrders_SearchByPhone_FindsOrder() { var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); var tenant = new TestTenant(cafeId, "user-1"); await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); var found = await orders.GetOpenOrdersAsync(cafeId, "09121111111", default); Assert.Single(found); Assert.Equal("09121111111", found[0].GuestPhone); } [Fact] public async Task Payment_CompletesOrder_TableBoardShowsFree() { var (_, orders, tables, cafeId, tableId, menuItemId) = CreateFixture(); var tenant = new TestTenant(cafeId, "user-1"); var created = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); var order = created.Data!; var paid = await orders.RecordPaymentsAsync( cafeId, order.Id, new RecordPaymentsRequest([new CreatePaymentRequest(PaymentMethod.Cash, order.Total, null)]), "user-1", default); Assert.True(paid.Success); var board = await tables.GetTableBoardAsync(cafeId, cancellationToken: default); var tile = board.First(t => t.Id == tableId); Assert.Equal(TableBoardStatus.Free, tile.Status); } [Fact] public async Task CleaningTable_BlocksNewOrder() { var (db, orders, tables, cafeId, tableId, menuItemId) = CreateFixture(); var table = await db.Tables.FindAsync(tableId); table!.IsCleaning = true; await db.SaveChangesAsync(); var tenant = new TestTenant(cafeId, "user-1"); var result = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); Assert.False(result.Success); Assert.Equal("TABLE_NOT_AVAILABLE", result.ErrorCode); } private sealed class TestTenant(string cafeId, string userId) : ITenantContext { public string? CafeId => cafeId; public string? UserId => userId; public EmployeeRole? Role => EmployeeRole.Owner; public PlanTier? PlanTier => Core.Enums.PlanTier.Pro; public string? Language => "fa"; public string? BranchId => null; public bool IsSystemAdmin => false; public bool IsAuthenticated => true; } private sealed class NoOpKdsNotifier : IKdsNotifier { public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => Task.CompletedTask; } private sealed class NoOpSnappfood : ISnappfoodClient { public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) => Task.CompletedTask; } }