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);
}
}