food-market/tests/food-market.IntegrationTests/Support/ApiFactory.cs
nns ac77849901 feat(reports): отчёт «Продажи» с группировками и экспортом (P1-8)
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>
2026-05-28 11:09:52 +05:00

65 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
});
});
}
}