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 AbcReportTests
{
private readonly ApiFactory _factory;
public AbcReportTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
/// 3 товара с явно разной долей выручки → классы A/B/C по Парето.
[Fact]
public async Task Pareto_distribution_A_B_C()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"abc-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var pBig = await api.CreateProductAsync(refs, $"BIG-{Guid.NewGuid():N}", 100m, RandomBarcode());
var pMed = await api.CreateProductAsync(refs, $"MED-{Guid.NewGuid():N}", 100m, RandomBarcode());
var pSmall = await api.CreateProductAsync(refs, $"SML-{Guid.NewGuid():N}", 100m, RandomBarcode());
// Подкормим склад.
foreach (var pid in new[] { pBig, pMed, pSmall })
{
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 = pid, quantity = 100m, unitCost = 10m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
}
// Продажи: BIG = 800, MED = 150, SML = 50 → итого 1000.
// A = накопительно 80% (BIG), B = 95% (+ MED = 95%), C = остаток (SML).
async Task Sale(string pid, decimal qty, decimal price)
{
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 = 0, paidCash = qty * price, paidCard = 0m,
lines = new[] { new { productId = pid, quantity = qty, unitPrice = price, discount = 0m, vatPercent = 12m } },
notes = "sale" });
resp.EnsureSuccessStatusCode();
var sid = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/sales/retail/{sid}/post", new { })).EnsureSuccessStatusCode();
}
await Sale(pBig, 8m, 100m);
await Sale(pMed, 15m, 10m);
await Sale(pSmall, 5m, 10m);
var rep = await api.GetJsonAsync("/api/reports/abc?metric=revenue");
var arr = rep.EnumerateArray().ToList();
arr.Should().HaveCountGreaterOrEqualTo(3);
var big = arr.First(x => x.GetProperty("productId").GetString() == pBig);
var med = arr.First(x => x.GetProperty("productId").GetString() == pMed);
var small = arr.First(x => x.GetProperty("productId").GetString() == pSmall);
big.GetProperty("abcClass").GetString().Should().Be("A");
med.GetProperty("abcClass").GetString().Should().Be("B");
small.GetProperty("abcClass").GetString().Should().Be("C");
// Накопительная доля C должна быть 100%.
small.GetProperty("cumulativeShare").GetDecimal().Should().BeApproximately(100m, 0.5m);
}
[Fact]
public async Task Empty_period_returns_empty()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"abc-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/abc?from={Uri.EscapeDataString(pastFrom)}&to={Uri.EscapeDataString(pastTo)}");
rep.EnumerateArray().Should().BeEmpty();
}
[Fact]
public async Task Profit_metric_orders_by_margin()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"abc-prft-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var supplierId = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}");
// pA: 5 шт по 100 — высокая выручка, но низкая маржа (cost 80).
// pB: 2 шт по 100 — низкая выручка, но высокая маржа (cost 10).
var pA = await api.CreateProductAsync(refs, $"A-{Guid.NewGuid():N}", 100m, RandomBarcode());
var pB = await api.CreateProductAsync(refs, $"B-{Guid.NewGuid():N}", 100m, RandomBarcode());
async Task Supply(string pid, decimal qty, decimal cost)
{
var s = await api.Http.PostAsJsonAsync("/api/purchases/supplies", new {
date = DateTime.UtcNow, supplierId, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "in", lines = new[] { new { productId = pid, quantity = qty, unitPrice = cost } } });
s.EnsureSuccessStatusCode();
var sid = (await s.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{sid}/post", new { })).EnsureSuccessStatusCode();
}
await Supply(pA, 10m, 80m);
await Supply(pB, 10m, 10m);
async Task Sale(string pid, decimal qty, decimal price)
{
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 = 0, paidCash = qty * price, paidCard = 0m,
lines = new[] { new { productId = pid, quantity = qty, unitPrice = price, discount = 0m, vatPercent = 12m } },
notes = "sale" });
resp.EnsureSuccessStatusCode();
var sid = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/sales/retail/{sid}/post", new { })).EnsureSuccessStatusCode();
}
await Sale(pA, 5m, 100m); // profit = 500 − 5*80 = 100
await Sale(pB, 2m, 100m); // profit = 200 − 2*10 = 180
// По revenue: A=500 > B=200, A первый (rank 1).
var byRev = await api.GetJsonAsync("/api/reports/abc?metric=revenue");
byRev.EnumerateArray().First().GetProperty("productId").GetString().Should().Be(pA);
// По profit: B=180 > A=100, B первый (rank 1).
var byProf = await api.GetJsonAsync("/api/reports/abc?metric=profit");
byProf.EnumerateArray().First().GetProperty("productId").GetString().Should().Be(pB);
}
[Fact]
public async Task Tenant_isolation_abc()
{
var a = new ApiActor(_factory.CreateClient());
var b = new ApiActor(_factory.CreateClient());
await a.SignupAndLoginAsync($"abc-iso-a-{Guid.NewGuid():N}");
await b.SignupAndLoginAsync($"abc-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 = 5m, unitCost = 10m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await a.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/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 = "s" });
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/abc");
repB.EnumerateArray().Should().NotContain(x => x.GetProperty("productId").GetString() == pA);
}
}