using Meezi.API.Models.Expenses; using Meezi.API.Services; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Xunit; namespace Meezi.API.Tests; public class ExpenseServiceTests { private static ( AppDbContext Db, ExpenseService Expenses, ShiftService Shifts, DailyReportService Reports, string CafeId, string BranchId, string UserId) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-exp"; var branchId = "branch-exp"; var userId = "mgr-1"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "Exp Cafe", Slug = "exp-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 = "Manager", Phone = "09123333333", Role = EmployeeRole.Manager }); db.SaveChanges(); var shifts = new ShiftService(db); var expenses = new ExpenseService(db, shifts); var reports = new DailyReportService(db, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); return (db, expenses, shifts, reports, cafeId, branchId, userId); } [Fact] public async Task CreateExpense_WithOpenShift_RecordsWithdrawal_AndReducesExpectedCash() { var (_, expenses, shifts, _, cafeId, branchId, userId) = CreateFixture(); var opened = await shifts.OpenShiftAsync(cafeId, branchId, 1_000_000m, userId); var shiftId = opened.Data!.Id; var created = await expenses.CreateExpenseAsync( cafeId, new CreateExpenseRequest(branchId, shiftId, ExpenseCategory.Supplies, 150_000m, "خرید شیر", null), userId); Assert.True(created.Success); Assert.Equal(150_000m, created.Data!.Amount); var shift = await shifts.CloseShiftAsync(cafeId, shiftId, 850_000m, userId); Assert.True(shift.Success); Assert.Equal(850_000m, shift.Data!.ExpectedCash); } [Fact] public async Task DeleteExpense_SoftDeletesExpenseAndWithdrawal() { var (db, expenses, shifts, _, cafeId, branchId, userId) = CreateFixture(); var opened = await shifts.OpenShiftAsync(cafeId, branchId, 500_000m, userId); var shiftId = opened.Data!.Id; var created = await expenses.CreateExpenseAsync( cafeId, new CreateExpenseRequest(branchId, shiftId, ExpenseCategory.Other, 50_000m, null, null), userId); Assert.True(created.Success); var deleted = await expenses.DeleteExpenseAsync(cafeId, created.Data!.Id); Assert.True(deleted.Success); var expenseRow = await db.Expenses.IgnoreQueryFilters() .FirstAsync(e => e.Id == created.Data.Id); Assert.NotNull(expenseRow.DeletedAt); var tx = await db.CashTransactions.IgnoreQueryFilters() .FirstAsync(t => t.ReferenceId == created.Data.Id); Assert.NotNull(tx.DeletedAt); } [Fact] public async Task DailyReport_Expense_ReducesNetIncome_NotRevenue() { var (db, expenses, shifts, reports, cafeId, branchId, userId) = CreateFixture(); var date = IranCalendar.TodayInIran; var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); var catId = "cat-1"; var itemId = "item-1"; 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 = "Tea", NameEn = "Tea", Price = 100_000m, IsAvailable = true }); var orderId = "ord-1"; db.Orders.Add(new Order { Id = orderId, CafeId = cafeId, BranchId = branchId, Status = OrderStatus.Delivered, OrderType = OrderType.DineIn, Subtotal = 100_000m, TaxTotal = 0, Total = 100_000m, CreatedAt = start.AddHours(10) }); db.OrderItems.Add(new OrderItem { OrderId = orderId, MenuItemId = itemId, Quantity = 1, UnitPrice = 100_000m }); await db.SaveChangesAsync(); await expenses.CreateExpenseAsync( cafeId, new CreateExpenseRequest(branchId, null, ExpenseCategory.Utilities, 30_000m, "برق", null), userId); var report = await reports.GenerateReportAsync(cafeId, branchId, date); Assert.Equal(100_000m, report.TotalRevenue); Assert.Equal(30_000m, report.TotalExpenses); Assert.Equal(70_000m, report.NetIncome); } }