using Meezi.API.Models.Orders; using Meezi.API.Services; 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 ShiftServiceTests { private static ( AppDbContext Db, ShiftService Shifts, OrderService Orders, string CafeId, string BranchId, string UserId, string MenuItemId) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-shift"; var branchId = "branch-main"; var userId = "emp-cashier"; var menuItemId = "item-1"; var categoryId = "cat-1"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "Shift Cafe", Slug = "shift-cafe", 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.MenuCategories.Add(new MenuCategory { Id = categoryId, CafeId = cafeId, Name = "Menu", NameEn = "Menu", SortOrder = 0 }); db.MenuItems.Add(new MenuItem { Id = menuItemId, CafeId = cafeId, CategoryId = categoryId, Name = "Tea", NameEn = "Tea", Price = 100_000m, IsAvailable = true }); db.SaveChanges(); var shifts = new ShiftService(db); var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); return (db, shifts, orders, cafeId, branchId, userId, menuItemId); } [Fact] public async Task OpenShift_Succeeds() { var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); var result = await shifts.OpenShiftAsync(cafeId, branchId, 500_000m, userId); Assert.True(result.Success); Assert.Equal(ShiftStatus.Open, result.Data!.Status); Assert.Equal(500_000m, result.Data.OpeningCash); } [Fact] public async Task OpenShift_WhenAlreadyOpen_ReturnsShiftAlreadyOpen() { var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); await shifts.OpenShiftAsync(cafeId, branchId, 100m, userId); var second = await shifts.OpenShiftAsync(cafeId, branchId, 200m, userId); Assert.False(second.Success); Assert.Equal("SHIFT_ALREADY_OPEN", second.ErrorCode); } [Fact] public async Task CloseShift_CalculatesDiscrepancy_FromCashPaymentsAndWithdrawals() { var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); var opened = await shifts.OpenShiftAsync(cafeId, branchId, 1_000_000m, userId); var shiftId = opened.Data!.Id; await shifts.RecordTransactionAsync( cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Cash, 300_000m, userId); await shifts.RecordTransactionAsync( cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Card, 200_000m, userId); await shifts.RecordTransactionAsync( cafeId, shiftId, CashTransactionType.Withdrawal, PaymentMethod.Cash, 50_000m, userId); var closed = await shifts.CloseShiftAsync(cafeId, shiftId, 1_300_000m, userId); Assert.True(closed.Success); Assert.Equal(1_250_000m, closed.Data!.ExpectedCash); Assert.Equal(50_000m, closed.Data.Discrepancy); Assert.Equal(ShiftStatus.Closed, closed.Data.Status); } [Fact] public async Task RecordPayments_WithOpenShift_RecordsCashTransactions() { var (db, shifts, orders, cafeId, branchId, userId, menuItemId) = CreateFixture(); await shifts.OpenShiftAsync(cafeId, branchId, 0m, userId); var tenant = new TenantContext { CafeId = cafeId, UserId = userId, Role = EmployeeRole.Cashier }; var created = await orders.CreateOrderAsync( cafeId, tenant, new CreateOrderRequest( OrderType.DineIn, branchId, null, null, "Guest", null, null, null, [new CreateOrderItemRequest(menuItemId, 1, null)])); Assert.True(created.Success); var pay = await orders.RecordPaymentsAsync( cafeId, created.Data!.Id, new RecordPaymentsRequest( [new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null), new CreatePaymentRequest(PaymentMethod.Card, 50_000m, null)]), userId); Assert.True(pay.Success); var shift = await db.RegisterShifts.Include(s => s.Transactions) .FirstAsync(s => s.BranchId == branchId && s.Status == ShiftStatus.Open); Assert.Equal(2, shift.Transactions.Count); Assert.Contains(shift.Transactions, t => t.Method == PaymentMethod.Cash && t.Amount == 100_000m); } [Fact] public async Task RecordPayments_WithoutOpenShift_ReturnsNoOpenShift() { var (_, _, orders, cafeId, branchId, userId, menuItemId) = CreateFixture(); var tenant = new TenantContext { CafeId = cafeId, UserId = userId }; var created = await orders.CreateOrderAsync( cafeId, tenant, new CreateOrderRequest( OrderType.DineIn, branchId, null, null, "Guest", null, null, null, [new CreateOrderItemRequest(menuItemId, 1, null)])); Assert.True(created.Success); var pay = await orders.RecordPaymentsAsync( cafeId, created.Data!.Id, new RecordPaymentsRequest([new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null)]), userId); Assert.False(pay.Success); Assert.Equal("NO_OPEN_SHIFT", pay.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; } }