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 NegativeStockTests { private readonly ApiFactory _factory; public NegativeStockTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); private async Task SeedStockAsync(ApiActor api, ApiActor.Refs refs, string productId, decimal qty) { var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "seed", lines = new[] { new { productId = productId, quantity = qty, unitCost = 50m } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); } [Fact] public async Task Loss_into_negative_is_allowed_creates_alert_recount_and_resolves_on_restock() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"neg-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); // 5 шт на складе, минусов нет. await SeedStockAsync(api, refs, p, 5m); (await api.StockOfAsync(refs.StoreId, p)).Should().Be(5m); var c0 = await api.GetJsonAsync("/api/negative-stock/count"); c0.GetProperty("total").GetInt32().Should().Be(0); // Списание 8 → остаток −3. Списание в минус РАЗРЕШЕНО (решение владельца). var loss = await api.Http.PostAsJsonAsync("/api/inventory/losses", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, reason = 0, notes = "тест минус", lines = new[] { new { productId = p, quantity = 8m, unitCost = 50m } }, }); loss.EnsureSuccessStatusCode(); var lid = (await loss.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); using var lpost = await api.Http.PostAsJsonAsync($"/api/inventory/losses/{lid}/post", new { }); lpost.IsSuccessStatusCode.Should().BeTrue($"списание в минус разрешено: {(int)lpost.StatusCode} {await lpost.Content.ReadAsStringAsync()}"); (await api.StockOfAsync(refs.StoreId, p)).Should().Be(-3m); // Предупреждение создано. var c1 = await api.GetJsonAsync("/api/negative-stock/count"); c1.GetProperty("total").GetInt32().Should().Be(1); var alerts = await api.ListAsync("/api/negative-stock"); alerts.Should().ContainSingle(); alerts[0].GetProperty("quantity").GetDecimal().Should().Be(-3m); alerts[0].GetProperty("productId").GetString().Should().Be(p); alerts[0].GetProperty("causeDocType").GetString().Should().Be("loss"); // «Пересчитать эти товары» → черновик инвентаризации с этим товаром. var rc = await api.Http.PostAsJsonAsync("/api/negative-stock/recount", new { storeId = refs.StoreId }); rc.EnsureSuccessStatusCode(); var rcj = await rc.Content.ReadFromJsonAsync(); rcj.GetProperty("lines").GetInt32().Should().Be(1); var invId = rcj.GetProperty("inventoryId").GetString(); var inv = await api.GetJsonAsync($"/api/inventory/inventories/{invId}"); inv.GetProperty("lines").EnumerateArray() .Should().ContainSingle(l => l.GetProperty("productId").GetString() == p); // Довоз 3 → остаток 0 → предупреждение закрылось автоматически. await SeedStockAsync(api, refs, p, 3m); (await api.StockOfAsync(refs.StoreId, p)).Should().Be(0m); var c2 = await api.GetJsonAsync("/api/negative-stock/count"); c2.GetProperty("total").GetInt32().Should().Be(0); (await api.ListAsync("/api/negative-stock")).Should().BeEmpty("минус закрыт — алерт resolved"); } [Fact] public async Task Negative_stock_report_filter_returns_only_negatives() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"neg-rep-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); await SeedStockAsync(api, refs, p, 2m); var loss = await api.Http.PostAsJsonAsync("/api/inventory/losses", new { date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, reason = 0, notes = "minus", lines = new[] { new { productId = p, quantity = 5m, unitCost = 50m } }, }); loss.EnsureSuccessStatusCode(); var lid = (await loss.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/losses/{lid}/post", new { })).EnsureSuccessStatusCode(); var neg = await api.GetJsonAsync("/api/inventory/stock?negativeOnly=true&pageSize=200"); var items = neg.GetProperty("items").EnumerateArray().ToList(); items.Should().NotBeEmpty(); items.Should().OnlyContain(x => x.GetProperty("quantity").GetDecimal() < 0); } }