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 SalesReportTests { private readonly ApiFactory _factory; public SalesReportTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); /// Создаёт 2 проведённых чека на разные товары и проверяет, что /// groupBy=product отдаёт обе позиции с правильной выручкой. [Fact] public async Task Group_by_product_returns_revenue_per_item() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"rpt-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var p1 = await api.CreateProductAsync(refs, $"P1-{Guid.NewGuid():N}", 200m, RandomBarcode()); var p2 = await api.CreateProductAsync(refs, $"P2-{Guid.NewGuid():N}", 300m, RandomBarcode()); // Принять по 10 шт каждый. foreach (var (pid, price, cost) in new[] { (p1, 200m, 100m), (p2, 300m, 150m) }) { 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 = 10m, unitCost = cost } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); } // Чек 1: 2 шт p1 (400) + 1 шт p2 (300) = 700 await PostSale(api, refs, new[] { (p1, 2m, 200m), (p2, 1m, 300m), }, paid: 700m); // Чек 2: 3 шт p1 (600) await PostSale(api, refs, new[] { (p1, 3m, 200m) }, paid: 600m); var rows = await api.GetJsonAsync("/api/reports/sales?groupBy=product"); var arr = rows.EnumerateArray().ToList(); arr.Should().HaveCountGreaterOrEqualTo(2); var p1Row = arr.First(x => x.GetProperty("key").GetString() == p1); p1Row.GetProperty("revenue").GetDecimal().Should().Be(1000m, "5 шт по 200"); p1Row.GetProperty("quantity").GetDecimal().Should().Be(5m); var p2Row = arr.First(x => x.GetProperty("key").GetString() == p2); p2Row.GetProperty("revenue").GetDecimal().Should().Be(300m); } [Fact] public async Task Group_by_payment_returns_per_method() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"rpt-pm-{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 = 20m, unitCost = 50m } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); // Cash на 200 + Card на 300. await PostSale(api, refs, new[] { (p1, 2m, 100m) }, paid: 200m, payment: 0); await PostSale(api, refs, new[] { (p1, 3m, 100m) }, paid: 0, paidCard: 300m, payment: 1); var rows = await api.GetJsonAsync("/api/reports/sales?groupBy=payment"); var arr = rows.EnumerateArray().ToList(); var cash = arr.First(x => x.GetProperty("key").GetString() == "0"); var card = arr.First(x => x.GetProperty("key").GetString() == "1"); cash.GetProperty("revenue").GetDecimal().Should().Be(200m); card.GetProperty("revenue").GetDecimal().Should().Be(300m); } [Fact] public async Task Returns_reduce_revenue_signed() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"rpt-ret-{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 = 5m, 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 saleId = await PostSale(api, refs, new[] { (p1, 5m, 100m) }, paid: 500m); // Создаём возврат на 2 шт. var crt = await api.Http.PostAsJsonAsync($"/api/sales/retail/{saleId}/create-return", new { }); crt.EnsureSuccessStatusCode(); var retId = (await crt.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); // Уменьшаем возврат до 2 шт (по умолчанию подтянулось 5). var put = await api.Http.PutAsJsonAsync($"/api/sales/retail/{retId}", new { date = DateTime.UtcNow, storeId = refs.StoreId, retailPointId = (string?)null, customerId = (string?)null, currencyId = refs.CurrencyId, payment = 0, paidCash = 200m, paidCard = 0m, notes = "ret", lines = new[] { new { productId = p1, quantity = 2m, unitPrice = 100m, discount = 0m, vatPercent = 12m } }, }); put.EnsureSuccessStatusCode(); (await api.Http.PostAsJsonAsync($"/api/sales/retail/{retId}/post", new { })).EnsureSuccessStatusCode(); var rows = await api.GetJsonAsync("/api/reports/sales?groupBy=product"); var p1Row = rows.EnumerateArray().First(x => x.GetProperty("key").GetString() == p1); // 5 шт по 100 минус 2 шт по 100 = 300. p1Row.GetProperty("revenue").GetDecimal().Should().Be(300m); p1Row.GetProperty("quantity").GetDecimal().Should().Be(3m); } [Fact] public async Task Tenant_isolation_sales_report() { var a = new ApiActor(_factory.CreateClient()); var b = new ApiActor(_factory.CreateClient()); await a.SignupAndLoginAsync($"rpt-iso-a-{Guid.NewGuid():N}"); await b.SignupAndLoginAsync($"rpt-iso-b-{Guid.NewGuid():N}"); var refsA = await a.LoadRefsAsync(); var pA = await a.CreateProductAsync(refsA, $"PA-{Guid.NewGuid():N}", 100m, RandomBarcode()); var enter = 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 = 10m } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await a.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); await PostSale(a, refsA, new[] { (pA, 1m, 100m) }, paid: 100m); // B запросил тот же отчёт — должен видеть пусто. var rowsB = await b.GetJsonAsync("/api/reports/sales?groupBy=product"); rowsB.EnumerateArray().Should().NotContain(x => x.GetProperty("key").GetString() == pA); } [Fact] public async Task Csv_export_works() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"rpt-csv-{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 = 5m, unitCost = 50m } }, }); enter.EnsureSuccessStatusCode(); var eid = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode(); await PostSale(api, refs, new[] { (p1, 2m, 100m) }, paid: 200m); using var resp = await api.Http.GetAsync("/api/reports/sales/export?groupBy=product&format=csv"); resp.EnsureSuccessStatusCode(); resp.Content.Headers.ContentType!.MediaType.Should().Be("text/csv"); var text = await resp.Content.ReadAsStringAsync(); text.Should().Contain("Выручка"); // русский заголовок text.Should().Contain("200"); } private async Task PostSale(ApiActor api, ApiActor.Refs refs, (string ProductId, decimal Qty, decimal Price)[] lines, decimal paid, decimal paidCard = 0m, int payment = 0) { var resp = await api.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId = refs.StoreId, retailPointId = (string?)null, customerId = (string?)null, currencyId = refs.CurrencyId, payment, paidCash = paid, paidCard, lines = lines.Select(l => new { productId = l.ProductId, quantity = l.Qty, unitPrice = l.Price, discount = 0m, vatPercent = 12m, }).ToArray(), notes = "test", }); resp.EnsureSuccessStatusCode(); var saleId = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!; (await api.Http.PostAsJsonAsync($"/api/sales/retail/{saleId}/post", new { })).EnsureSuccessStatusCode(); return saleId; } }