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 InventoryPostUnpostTests { private readonly ApiFactory _factory; public InventoryPostUnpostTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); [Fact] public async Task Create_loads_book_qty_post_applies_diff_to_stock() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"inv-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P1-{Guid.NewGuid():N}", 100m, RandomBarcode()); var p2 = await api.CreateProductAsync(refs, $"P2-{Guid.NewGuid():N}", 200m, RandomBarcode()); // Подкладываем 10 шт p1 и 5 шт p2 через Enter. foreach (var (pid, qty, cost) in new[] { (p1, 10m, 50m), (p2, 5m, 100m) }) { 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 = pid, quantity = qty, unitCost = cost } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); } // Создаём инвентаризацию без строк — контроллер подгрузит сам. var create = await api.Http.PostAsJsonAsync("/api/inventory/inventories", new { date = DateTime.UtcNow, storeId = refs.StoreId, notes = "ежемесячный пересчёт", lines = (object[])Array.Empty(), }); create.EnsureSuccessStatusCode(); var inv = await create.Content.ReadFromJsonAsync(); var invId = inv.GetProperty("id").GetString(); var lines = inv.GetProperty("lines").EnumerateArray().ToList(); lines.Should().HaveCountGreaterOrEqualTo(2, "контроллер подгрузил bookQty для всех товаров склада"); var lineP1 = lines.First(l => l.GetProperty("productId").GetString() == p1); lineP1.GetProperty("bookQty").GetDecimal().Should().Be(10m); lineP1.GetProperty("actualQty").GetDecimal().Should().Be(0m); // Вносим фактические: 12 шт p1 (излишек +2), 3 шт p2 (недостача -2). var put = await api.Http.PutAsJsonAsync($"/api/inventory/inventories/{invId}", new { date = DateTime.UtcNow, storeId = refs.StoreId, notes = "проведено", lines = new[] { new { productId = p1, actualQty = 12m }, new { productId = p2, actualQty = 3m }, }, }); put.EnsureSuccessStatusCode(); // Post → стоки 12 / 3. using var post = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/post", new { }); post.IsSuccessStatusCode.Should().BeTrue($"post вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}"); (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(12m); (await api.StockOfAsync(refs.StoreId, p2)).Should().Be(3m); // Unpost → откат к 10/5. using var unpost = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/unpost", new { }); unpost.IsSuccessStatusCode.Should().BeTrue(); (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(10m); (await api.StockOfAsync(refs.StoreId, p2)).Should().Be(5m); } [Fact] public async Task Post_rejected_if_no_diffs() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"inv-nodiff-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); 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 = p1, quantity = 3m, unitCost = 50m } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); var create = await api.Http.PostAsJsonAsync("/api/inventory/inventories", new { date = DateTime.UtcNow, storeId = refs.StoreId, notes = "match", lines = (object[])Array.Empty(), }); create.EnsureSuccessStatusCode(); var inv = await create.Content.ReadFromJsonAsync(); var invId = inv.GetProperty("id").GetString(); // Фактическое = учётному (3 == 3, diff = 0). await api.Http.PutAsJsonAsync($"/api/inventory/inventories/{invId}", new { date = DateTime.UtcNow, storeId = refs.StoreId, notes = "no-diff", lines = new[] { new { productId = p1, actualQty = 3m } }, }); using var post = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/post", new { }); ((int)post.StatusCode).Should().Be(400); } [Fact] public async Task Tenant_isolation_inventory() { var a = new ApiActor(_factory.CreateClient()); var b = new ApiActor(_factory.CreateClient()); await a.SignupAndLoginAsync($"inv-iso-a-{Guid.NewGuid():N}"); await b.SignupAndLoginAsync($"inv-iso-b-{Guid.NewGuid():N}"); var refsA = await a.LoadRefsAsync(); var resp = await a.Http.PostAsJsonAsync("/api/inventory/inventories", new { date = DateTime.UtcNow, storeId = refsA.StoreId, notes = "iso", lines = (object[])Array.Empty(), }); resp.EnsureSuccessStatusCode(); var invId = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); var bList = await b.ListAsync("/api/inventory/inventories?pageSize=200"); bList.Should().NotContain(x => x.GetProperty("id").GetString() == invId); using var direct = await b.Http.GetAsync($"/api/inventory/inventories/{invId}"); direct.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound); } }