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 ProfitReportTests { private readonly ApiFactory _factory; public ProfitReportTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); /// Купили 10 шт по 50 (cost после Supply post = 50), продали 4 шт по /// 100. Прибыль = 400 − 200 = 200, маржа = 50%. [Fact] public async Task Profit_simple_supply_sale() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"prft-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplierId = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode()); // Приёмка: 10 шт по 50. var sup = await api.Http.PostAsJsonAsync("/api/purchases/supplies", new { date = DateTime.UtcNow, supplierId, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = "init", lines = new[] { new { productId = p1, quantity = 10m, unitPrice = 50m } } }); sup.EnsureSuccessStatusCode(); var supId = (await sup.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supId}/post", new { })).EnsureSuccessStatusCode(); // Продажа: 4 шт по 100. var sale = await api.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId = refs.StoreId, retailPointId = (string?)null, customerId = (string?)null, currencyId = refs.CurrencyId, payment = 0, paidCash = 400m, paidCard = 0m, lines = new[] { new { productId = p1, quantity = 4m, unitPrice = 100m, discount = 0m, vatPercent = 12m } }, notes = "sale" }); sale.EnsureSuccessStatusCode(); var sid = (await sale.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/sales/retail/{sid}/post", new { })).EnsureSuccessStatusCode(); var rep = await api.GetJsonAsync("/api/reports/profit?groupBy=product"); var r = rep.EnumerateArray().First(x => x.GetProperty("key").GetString() == p1); r.GetProperty("revenue").GetDecimal().Should().Be(400m); r.GetProperty("cost").GetDecimal().Should().Be(200m); r.GetProperty("profit").GetDecimal().Should().Be(200m); r.GetProperty("marginPercent").GetDecimal().Should().Be(50m); } /// Защита от деления на ноль: при нулевой выручке (продажи=0) margin=0, /// а не NaN/Infinity. Проверяем что хотя бы пустой набор возвращается без 500. [Fact] public async Task Empty_period_returns_no_rows_no_division_by_zero() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"prft-emp-{Guid.NewGuid():N}"); var pastFrom = DateTime.UtcNow.AddYears(-5).ToString("o"); var pastTo = DateTime.UtcNow.AddYears(-4).ToString("o"); var rep = await api.GetJsonAsync($"/api/reports/profit?groupBy=product&from={Uri.EscapeDataString(pastFrom)}&to={Uri.EscapeDataString(pastTo)}"); rep.EnumerateArray().Should().BeEmpty(); } [Fact] public async Task Tenant_isolation_profit() { var a = new ApiActor(_factory.CreateClient()); var b = new ApiActor(_factory.CreateClient()); await a.SignupAndLoginAsync($"prft-iso-a-{Guid.NewGuid():N}"); await b.SignupAndLoginAsync($"prft-iso-b-{Guid.NewGuid():N}"); var refsA = await a.LoadRefsAsync(); var supplierA = await a.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var pA = await a.CreateProductAsync(refsA, $"PA-{Guid.NewGuid():N}", 100m, RandomBarcode()); var sup = await a.Http.PostAsJsonAsync("/api/purchases/supplies", new { date = DateTime.UtcNow, supplierId = supplierA, storeId = refsA.StoreId, currencyId = refsA.CurrencyId, notes = "init", lines = new[] { new { productId = pA, quantity = 5m, unitPrice = 20m } } }); sup.EnsureSuccessStatusCode(); var supId = (await sup.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await a.Http.PostAsJsonAsync($"/api/purchases/supplies/{supId}/post", new { })).EnsureSuccessStatusCode(); var sale = await a.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId = refsA.StoreId, retailPointId = (string?)null, customerId = (string?)null, currencyId = refsA.CurrencyId, payment = 0, paidCash = 100m, paidCard = 0m, lines = new[] { new { productId = pA, quantity = 1m, unitPrice = 100m, discount = 0m, vatPercent = 12m } }, notes = "sale" }); sale.EnsureSuccessStatusCode(); var sid = (await sale.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await a.Http.PostAsJsonAsync($"/api/sales/retail/{sid}/post", new { })).EnsureSuccessStatusCode(); var repB = await b.GetJsonAsync("/api/reports/profit?groupBy=product"); repB.EnumerateArray().Should().NotContain(x => x.GetProperty("key").GetString() == pA); } }