using Meezi.API.Models.Orders; using Meezi.API.Models.Shifts; using Meezi.API.Services; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using TenantContext = Meezi.Infrastructure.Data.TenantContext; using Microsoft.EntityFrameworkCore; using Xunit; namespace Meezi.API.Tests; public class OrderVoidTransferTests { private static ( AppDbContext Db, OrderService Orders, string CafeId, string Table1Id, string Table2Id, string MenuItemId, TenantContext Tenant) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-void"; var table1 = "table-1"; var table2 = "table-2"; var menuItemId = "item-1"; var categoryId = "cat-1"; var managerId = "emp-manager"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test-void", PlanTier = PlanTier.Pro }); db.Tables.Add(new Table { Id = table1, CafeId = cafeId, Number = "1", QrCode = "qr1", Capacity = 4 }); db.Tables.Add(new Table { Id = table2, CafeId = cafeId, Number = "2", QrCode = "qr2", 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 = "Tea", NameEn = "Tea", Price = 50_000m, IsAvailable = true }); db.Employees.Add(new Employee { Id = managerId, CafeId = cafeId, Name = "Manager", Phone = "09120000000", Role = EmployeeRole.Manager }); db.SaveChanges(); var tenant = new TenantContext { CafeId = cafeId, UserId = managerId, Role = EmployeeRole.Manager, PlanTier = PlanTier.Pro, Language = "fa" }; var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), new NoOpShiftService(), TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); return (db, orders, cafeId, table1, table2, menuItemId, tenant); } private static async Task CreateOpenOrderAsync( OrderService orders, string cafeId, TenantContext tenant, string tableId, string menuItemId, int qty = 1) { var result = await orders.CreateOrderAsync( cafeId, tenant, new CreateOrderRequest( OrderType.DineIn, null, tableId, null, "Guest", null, null, null, [new CreateOrderItemRequest(menuItemId, qty, null)])); Assert.True(result.Success); return result.Data!; } [Fact] public async Task VoidItem_ReducesOrderTotal() { var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture(); var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId, 2); var itemId = order.Items[0].Id; var before = order.Total; var result = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); Assert.True(result.Success); Assert.True(result.Data!.Items.First(i => i.Id == itemId).IsVoided); Assert.True(result.Data.Total < before); } [Fact] public async Task VoidItem_AlreadyVoided_ReturnsError() { var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture(); var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); var itemId = order.Items[0].Id; await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); var second = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); Assert.False(second.Success); Assert.Equal("ITEM_ALREADY_VOIDED", second.ErrorCode); } [Fact] public async Task TransferTable_MovesOrderToFreeTable() { var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); var result = await orders.TransferTableAsync(cafeId, order.Id, table2); Assert.True(result.Success); Assert.Equal(table2, result.Data!.TableId); } [Fact] public async Task TransferTable_ToOccupiedTable_ReturnsTableOccupied() { var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); var order2 = await CreateOpenOrderAsync(orders, cafeId, tenant, table2, menuItemId); var result = await orders.TransferTableAsync(cafeId, order2.Id, table1); Assert.False(result.Success); Assert.Equal("TABLE_OCCUPIED", result.ErrorCode); } [Fact] public async Task TransferTable_ToCleaningTable_ReturnsTableCleaning() { var (db, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); var table = await db.Tables.FirstAsync(t => t.Id == table2); table.IsCleaning = true; await db.SaveChangesAsync(); var result = await orders.TransferTableAsync(cafeId, order.Id, table2); Assert.False(result.Success); Assert.Equal("TABLE_CLEANING", result.ErrorCode); } 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; } private sealed class NoOpShiftService : IShiftService { public Task> OpenShiftAsync(string cafeId, string branchId, decimal openingCash, string userId, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> CloseShiftAsync(string cafeId, string shiftId, decimal closingCash, string userId, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task GetCurrentShiftAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) => Task.FromResult(null); public Task?> GetTransactionsAsync(string cafeId, string shiftId, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> RecordTransactionAsync(string cafeId, string shiftId, CashTransactionType type, PaymentMethod method, decimal amount, string createdByUserId, string? referenceId = null, string? note = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> RequireOpenShiftForBranchAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) => Task.FromResult(new ShiftServiceResult(true, new Shift { Id = "shift-test", CafeId = cafeId, BranchId = branchId })); } }