GET /api/reports/sales — агрегаты по period:day/week/month, product, cashier, register, payment. Фильтры: from/to (по умолчанию last 30 days), storeId, productGroupId. Возвраты включаются с минусом (netto-выручка для фискальной отчётности). GET /api/reports/sales/export?format=csv|xlsx — выгрузка через CsvHelper (BOM UTF-8 + ; разделитель для Excel-RU) и ClosedXML. Реализация: плоский набор строк проектируется на сервере БД (Join+Where, EF переводит), агрегация в C#. Сознательный компромисс — EF8 не переводит «distinct count» внутри group-проекции с join'ами по nullable-ключам; объёмы отчётов (~десятки тысяч строк/месяц) держатся в RAM спокойно. Web: /reports/sales — выбор периода, табы группировки, фильтры, экспорт. Sidebar: «Отчёты → Продажи» для Admin/Storekeeper. Bonus: попутно вылечен баг RetailSalesController.Update — DbUpdateConcurrency «0 affected» воспроизводился при PUT на свеже-созданный возврат (create-return + immediate edit). Исправлено двумя изменениями: • Update не делает Include(Lines) — старые строки удаляются ExecuteDelete'ом; • ApplyLines добавляет новые строки напрямую в DbSet (а не через nav-collection sale.Lines.Add) — иначе EF8 путается со state'ом из-за client-side Id (Guid). Тесты: 5 интеграционных (group by product, group by payment, returns reduce revenue signed, tenant isolation, CSV export). 37 интеграционных всего зелёные. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
65 lines
3.6 KiB
C#
65 lines
3.6 KiB
C#
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.AspNetCore.Mvc.Testing;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Testcontainers.PostgreSql;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests.Support;
|
||
|
||
/// <summary>Поднимает реальный API (WebApplicationFactory) поверх одноразового
|
||
/// Postgres-контейнера (Testcontainers). Миграции применяются на старте host'а
|
||
/// (Program.Migrate), сидеры наполняют справочники и SuperAdmin. Запускается
|
||
/// один раз на всю коллекцию тестов.</summary>
|
||
public sealed class ApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||
{
|
||
static ApiFactory()
|
||
{
|
||
// Ryuk (reaper) тянет образ из Docker Hub — на этом хосте сеть к внешним
|
||
// реестрам нестабильна, а postgres:16-alpine уже закэширован. Выключаем.
|
||
Environment.SetEnvironmentVariable("TESTCONTAINERS_RYUK_DISABLED", "true");
|
||
// Лимитер читает конфиг ЭАГЕРНО при регистрации сервисов — поэтому
|
||
// переопределяем через env-переменную (её CreateBuilder подхватывает до
|
||
// регистрации), а не через ConfigureAppConfiguration (применяется позже).
|
||
// Тесты логинятся десятки раз с одного loopback-IP — иначе 429.
|
||
Environment.SetEnvironmentVariable("RateLimiting__Enabled", "false");
|
||
// Тише логи в тестовом прогоне (OpenIddict сыплет Debug-событиями).
|
||
// Ошибки (Error) пропускаем — нужно для отладки 500 в тестах.
|
||
Environment.SetEnvironmentVariable("Serilog__MinimumLevel__Default", "Warning");
|
||
// Hangfire-сервер не нужен в интеграционных тестах: он бы создавал свои
|
||
// таблицы в одноразовом контейнере и удерживал коннекшен. Сам клиент
|
||
// (AddHangfire) остаётся — recurring jobs не регистрируются.
|
||
Environment.SetEnvironmentVariable("Hangfire__Enabled", "false");
|
||
}
|
||
|
||
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder()
|
||
.WithImage("postgres:16-alpine")
|
||
.WithDatabase("food_market")
|
||
.WithUsername("food_market")
|
||
.WithPassword("food_market_test")
|
||
.Build();
|
||
|
||
public async Task InitializeAsync() => await _db.StartAsync();
|
||
|
||
public new async Task DisposeAsync()
|
||
{
|
||
await _db.DisposeAsync();
|
||
await base.DisposeAsync();
|
||
}
|
||
|
||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||
{
|
||
// Development — простые dev-ключи OpenIddict (без генерации сертификатов),
|
||
// 3-сегментный токен. Лимитер выключаем: тесты логинятся много раз с одного IP.
|
||
builder.UseEnvironment("Development");
|
||
builder.ConfigureAppConfiguration((_, cfg) =>
|
||
{
|
||
// Строку подключения AddDbContext читает лениво — здесь override
|
||
// срабатывает. RateLimiting выключён через env (см. static ctor).
|
||
cfg.AddInMemoryCollection(new Dictionary<string, string?>
|
||
{
|
||
["ConnectionStrings:Default"] = _db.GetConnectionString(),
|
||
});
|
||
});
|
||
}
|
||
}
|