using ERPCore.Domain.Entities; using ERPCore.Domain.Enums; using ERPCore.Dtos.Common; using ERPCore.Infra.Persistence; using ERPCore.Repositories; using ERPCore.Services.Stock; using static ERPCore.Tests.TestSupport.BankDepositTestSupport; namespace ERPCore.Tests; /// The Stock Ledger screen: item name/SKU/category/brand on every row, category and brand filters, and a running /// balance that is the real balance after each movement (not whatever a posting stored). public class StockLedgerTests { private static StockService Svc(ErpDbContext db) => new(null!, new Repository(db), new Repository(db), new Repository(db), new Repository(db), new Repository(db)); private static StockLedger Row(int id, int item, int warehouse, Direction dir, decimal qty, decimal unitCost, decimal storedBalance = 0m) => new() { LedgerId = id, ItemId = item, WarehouseId = warehouse, Direction = dir, QtyBase = qty, UnitCost = unitCost, Value = qty * unitCost, RunningBalance = storedBalance, SourceDocType = "T", SourceDocId = id, UserId = 1, CreatedAt = new DateTime(2026, 10, 18, 4, 0, 0, DateTimeKind.Utc).AddMinutes(id) }; private static async Task SeedAsync() { var db = CreateContext(); db.Categories.AddRange(new Category { CategoryId = 1, Name = "Beverages" }, new Category { CategoryId = 2, Name = "Snacks" }); db.Brands.AddRange(new Brand { BrandId = 1, Name = "Milo" }, new Brand { BrandId = 2, Name = "Munchee" }); db.Items.AddRange( new Item { ItemId = 1, Sku = "MILO-240", Name = "Milo 240ml", CategoryId = 1, BrandId = 1, BaseUomId = 1 }, new Item { ItemId = 2, Sku = "BISC-1", Name = "Biscuit", CategoryId = 2, BrandId = 2, BaseUomId = 1 }); db.StockLedger.AddRange( Row(1, 1, Warehouse1, Direction.In, 100m, 200m, storedBalance: 100m), Row(2, 1, Warehouse1, Direction.Out, 30m, 200m, storedBalance: 0m), // a sales issue: stored balance was never filled in Row(3, 1, Warehouse1, Direction.In, 50m, 210m, storedBalance: 120m), Row(4, 1, Warehouse2, Direction.In, 10m, 200m, storedBalance: 10m), Row(5, 2, Warehouse1, Direction.In, 5m, 40m, storedBalance: 5m)); await db.SaveChangesAsync(); return db; } private static Task> Ledger( ErpDbContext db, int page = 1, int pageSize = 50, int? itemId = null, int? warehouseId = null, int? categoryId = null, int? brandId = null) => Svc(db).GetLedgerAsync(itemId, warehouseId, null, null, null, null, null, new PageQuery { Page = page, PageSize = pageSize }, CancellationToken.None, categoryId, brandId); [Fact] public async Task RunningBalance_IsTheRealBalanceAfterEachMovement_PerItemAndWarehouse() { await using var db = await SeedAsync(); var rows = (await Ledger(db)).Items.ToDictionary(r => r.LedgerId); Assert.Equal(100m, rows[1].RunningBalance); Assert.Equal(70m, rows[2].RunningBalance); // the stored 0 is ignored Assert.Equal(120m, rows[3].RunningBalance); Assert.Equal(10m, rows[4].RunningBalance); // another warehouse starts from its own zero Assert.Equal(5m, rows[5].RunningBalance); // another item too } [Fact] public async Task RunningBalance_IsStillRight_OnALaterPage_AndWithFiltersApplied() { await using var db = await SeedAsync(); // Newest first, 2 per page: page 2 holds ledger 3 and 2, whose balances depend on rows before and after it. var page2 = (await Ledger(db, page: 2, pageSize: 2)).Items; Assert.Equal(new long[] { 3, 2 }, page2.Select(r => (long)r.LedgerId)); Assert.Equal(new[] { 120m, 70m }, page2.Select(r => r.RunningBalance)); // Filtering to one warehouse and item changes which rows show, not what the balance is. var only = (await Ledger(db, itemId: 1, warehouseId: Warehouse1)).Items.OrderBy(r => r.LedgerId).ToList(); Assert.Equal(new[] { 100m, 70m, 120m }, only.Select(r => r.RunningBalance)); } [Fact] public async Task EveryRow_CarriesTheItemsSkuNameCategoryAndBrand() { await using var db = await SeedAsync(); var row = (await Ledger(db, itemId: 1)).Items.First(); Assert.Equal(("MILO-240", "Milo 240ml", "Beverages", "Milo"), (row.ItemSku, row.ItemName, row.CategoryName, row.BrandName)); } [Fact] public async Task CategoryAndBrandFilters_NarrowTheLedgerToThoseItems() { await using var db = await SeedAsync(); Assert.All((await Ledger(db, categoryId: 2)).Items, r => Assert.Equal(2, r.ItemId)); Assert.Equal(4, (await Ledger(db, brandId: 1)).Items.Count); Assert.Empty((await Ledger(db, categoryId: 2, brandId: 1)).Items); } }