using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; [Collection(ApiCollection.Name)] public class StockReportTests { private readonly ApiFactory _factory; public StockReportTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); /// Реконструкция остатков на «сейчас» совпадает с текущим Stock. [Fact] public async Task Today_matches_current_stock() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"strpt-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "seed", lines = new[] { new { productId = p1, quantity = 7m, unitCost = 30m } } }); e.EnsureSuccessStatusCode(); var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); var rep = await api.GetJsonAsync("/api/reports/stock"); var rows = rep.EnumerateArray().ToList(); var r = rows.First(x => x.GetProperty("productId").GetString() == p1); r.GetProperty("quantity").GetDecimal().Should().Be(7m); r.GetProperty("cost").GetDecimal().Should().Be(30m); r.GetProperty("value").GetDecimal().Should().Be(210m); } /// Запрос на дату до первой операции — пусто. [Fact] public async Task Date_before_first_movement_returns_empty() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"strpt-pre-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); // Принять товар прямо сейчас. var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "seed", lines = new[] { new { productId = p1, quantity = 3m, unitCost = 10m } } }); e.EnsureSuccessStatusCode(); var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); // Запрос на год назад — ничего. var pastDate = DateTime.UtcNow.AddYears(-1).ToString("o"); var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(pastDate)}"); var rows = rep.EnumerateArray().ToList(); rows.Should().NotContain(x => x.GetProperty("productId").GetString() == p1); } /// Дата в будущем — отчёт = текущий остаток. [Fact] public async Task Future_date_equals_current() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"strpt-fut-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "seed", lines = new[] { new { productId = p1, quantity = 5m, unitCost = 20m } } }); e.EnsureSuccessStatusCode(); var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); var future = DateTime.UtcNow.AddYears(1).ToString("o"); var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(future)}"); var r = rep.EnumerateArray().First(x => x.GetProperty("productId").GetString() == p1); r.GetProperty("quantity").GetDecimal().Should().Be(5m); } /// Снимок «вчера» — без учёта сегодняшней операции. [Fact] public async Task Date_before_operation_excludes_it() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"strpt-bef-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); // Принять с future-датой (так контроллер использует входящую дату). var date = DateTime.UtcNow.AddDays(2); var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new { date, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "future-supply", lines = new[] { new { productId = p1, quantity = 4m, unitCost = 10m } } }); e.EnsureSuccessStatusCode(); var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); // На сейчас (раньше OccurredAt = date) — товара ещё нет. var now = DateTime.UtcNow.ToString("o"); var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(now)}"); var rows = rep.EnumerateArray().ToList(); rows.Should().NotContain(x => x.GetProperty("productId").GetString() == p1); // На дату ПОСЛЕ операции — товар появился. var after = date.AddDays(1).ToString("o"); var rep2 = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(after)}"); var r = rep2.EnumerateArray().First(x => x.GetProperty("productId").GetString() == p1); r.GetProperty("quantity").GetDecimal().Should().Be(4m); } [Fact] public async Task Tenant_isolation_stock_report() { var a = new ApiActor(_factory.CreateClient()); var b = new ApiActor(_factory.CreateClient()); await a.SignupAndLoginAsync($"strpt-iso-a-{Guid.NewGuid():N}"); await b.SignupAndLoginAsync($"strpt-iso-b-{Guid.NewGuid():N}"); var refsA = await a.LoadRefsAsync(); var pA = await a.CreateProductAsync(refsA, $"PA-{Guid.NewGuid():N}", 100m, RandomBarcode()); var e = await a.Http.PostAsJsonAsync("/api/inventory/enters", new { date = DateTime.UtcNow, storeId = refsA.StoreId, currencyId = refsA.CurrencyId, notes = "seed", lines = new[] { new { productId = pA, quantity = 1m, unitCost = 5m } } }); e.EnsureSuccessStatusCode(); var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await a.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); var repB = await b.GetJsonAsync("/api/reports/stock"); repB.EnumerateArray().Should().NotContain(x => x.GetProperty("productId").GetString() == pA); } }