using Meezi.API.Services; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Meezi.API.Tests; public class DailyReportServiceTests { private static (AppDbContext Db, DailyReportService Service, string CafeId, string BranchId, string ItemId) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-report"; var branchId = "branch-1"; var branch2 = "branch-2"; var itemId = "item-espresso"; var catId = "cat-1"; var userId = "emp-1"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "R", Slug = "r", PlanTier = PlanTier.Business }); db.Branches.Add(new Branch { Id = branchId, CafeId = cafeId, Name = "A", IsActive = true, UpdatedAt = DateTime.UtcNow }); db.Branches.Add(new Branch { Id = branch2, CafeId = cafeId, Name = "B", IsActive = true, UpdatedAt = DateTime.UtcNow }); db.Employees.Add(new Employee { Id = userId, CafeId = cafeId, BranchId = branchId, Name = "E", Phone = "09120000001", Role = EmployeeRole.Owner }); db.MenuCategories.Add(new MenuCategory { Id = catId, CafeId = cafeId, Name = "C", NameEn = "C", SortOrder = 0 }); db.MenuItems.Add(new MenuItem { Id = itemId, CafeId = cafeId, CategoryId = catId, Name = "Espresso", NameEn = "Espresso", Price = 50_000m, IsAvailable = true }); db.SaveChanges(); var service = new DailyReportService(db, NullLogger.Instance); return (db, service, cafeId, branchId, itemId); } private static async Task SeedClosedOrderAsync( AppDbContext db, string cafeId, string branchId, string itemId, DateTime createdAt, bool voidFirstLine, IReadOnlyList<(PaymentMethod Method, decimal Amount)>? payments = null, int activeQty = 1) { var orderId = $"ord_{Guid.NewGuid():N}"[..16]; var activeRevenue = 50_000m * activeQty; var order = new Order { Id = orderId, CafeId = cafeId, BranchId = branchId, Status = OrderStatus.Delivered, OrderType = OrderType.DineIn, Subtotal = activeRevenue, TaxTotal = 0, Total = activeRevenue, CreatedAt = createdAt }; db.Orders.Add(order); if (voidFirstLine) { db.OrderItems.Add(new OrderItem { OrderId = orderId, MenuItemId = itemId, Quantity = 2, UnitPrice = 50_000m, IsVoided = true, VoidedAt = createdAt }); } db.OrderItems.Add(new OrderItem { OrderId = orderId, MenuItemId = itemId, Quantity = activeQty, UnitPrice = 50_000m }); if (payments is not null) { var shiftId = $"shift_{Guid.NewGuid():N}"[..16]; db.RegisterShifts.Add(new Shift { Id = shiftId, CafeId = cafeId, BranchId = branchId, OpenedByUserId = "emp-1", OpenedAt = createdAt.AddHours(-1), OpeningCash = 0, Status = ShiftStatus.Open, CreatedAt = createdAt.AddHours(-1) }); foreach (var p in payments) { db.CashTransactions.Add(new CashTransaction { CafeId = cafeId, BranchId = branchId, ShiftId = shiftId, Type = CashTransactionType.OrderPayment, Method = p.Method, Amount = p.Amount, ReferenceId = orderId, CreatedByUserId = "emp-1", CreatedAt = createdAt.AddMinutes(5) }); } } await db.SaveChangesAsync(); } [Fact] public async Task GenerateReport_IsIdempotent() { var (db, service, cafeId, branchId, itemId) = CreateFixture(); var date = IranCalendar.TodayInIran.AddDays(-1); var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(12), voidFirstLine: false); var first = await service.GenerateReportAsync(cafeId, branchId, date); var second = await service.GenerateReportAsync(cafeId, branchId, date); Assert.Equal(first.Id, second.Id); Assert.Equal(first.TotalRevenue, second.TotalRevenue); Assert.Equal(1, await db.DailyReports.CountAsync(r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date)); } [Fact] public async Task GenerateReport_ExcludesVoidedItemsFromRevenue() { var (db, service, cafeId, branchId, itemId) = CreateFixture(); var date = IranCalendar.TodayInIran; var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(10), voidFirstLine: true); var report = await service.GenerateReportAsync(cafeId, branchId, date); Assert.Equal(50_000m, report.TotalRevenue); Assert.Equal(1, report.TotalVoids); Assert.Equal(100_000m, report.VoidAmount); } [Fact] public async Task GenerateReport_SumsPaymentMethods_FromCashTransactions() { var (db, service, cafeId, branchId, itemId) = CreateFixture(); var date = IranCalendar.TodayInIran; var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); await SeedClosedOrderAsync( db, cafeId, branchId, itemId, start.AddHours(11), false, payments: [ (PaymentMethod.Cash, 60_000m), (PaymentMethod.Card, 40_000m), (PaymentMethod.Credit, 50_000m) ]); var report = await service.GenerateReportAsync(cafeId, branchId, date); Assert.Equal(60_000m, report.CashRevenue); Assert.Equal(40_000m, report.CardRevenue); Assert.Equal(50_000m, report.CreditRevenue); } [Fact] public async Task GetSummary_AggregatesAcrossBranches() { var (db, service, cafeId, branchId, itemId) = CreateFixture(); var branch2 = "branch-2"; var date = IranCalendar.TodayInIran; var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(9), false); await SeedClosedOrderAsync(db, cafeId, branch2, itemId, start.AddHours(10), false); await service.GenerateReportAsync(cafeId, branchId, date); await service.GenerateReportAsync(cafeId, branch2, date); var summary = await service.GetSummaryAsync(cafeId, 30); Assert.Equal(2, summary.ByBranch.Count); Assert.Equal(100_000m, summary.TotalRevenue); Assert.Equal(100_000m, summary.ByBranch.Sum(b => b.TotalRevenue)); Assert.Equal(2, summary.TotalOrders); } [Fact] public void ReportPlanGate_Free_AllowsEightDayWindow() { var today = new DateOnly(2026, 5, 21); const int freeMaxDays = 8; // Free plan's report-history window Assert.True(ReportPlanGate.IsDateInRange(freeMaxDays, today, today)); Assert.True(ReportPlanGate.IsDateInRange(freeMaxDays, today.AddDays(-7), today)); Assert.False(ReportPlanGate.IsDateInRange(freeMaxDays, today.AddDays(-8), today)); } }