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;
}
}