food-market/tests/e2e/reports/full-cycle-2026-05-08-full-pass.md
nns 37cd9aa94b test(e2e): починка контрактов supply/sale + EAN-13 + bug-hunt + full-pass отчёт
Контракты до фикса не совпадали с реальными:
- Product: unitId/groupId/retailPrice → unitOfMeasureId/productGroupId/prices[],
  плюс обязательный barcodes[] (генерим валидный EAN-13).
- Supply: counterpartyId/docDate/lines.price → supplierId/date/lines.unitPrice,
  плюс обязательный currencyId.
- RetailSale: путь /api/sales/retail-sales 404 → /api/sales/retail; payload
  обновлён под RetailSaleInput (storeId, currencyId, payment, paidCash и т.п.).

Шаги 9-12 теперь полностью проходят (не skip). Добавлены deep-bug-hunt'ы:
- Supply без supplierId / с пустым lines[]
- двойной post Supply / RetailSale → 409
- stock_movements vs Stocks.Quantity консистентность
- RetailPoint с несуществующим storeId
- продажа qty>остатка (выявил блокирующий баг — продаёт)
- discount на line, отрицательные qty/price
- stock_movements.Type = RetailSale (2)

Отчёт: tests/e2e/reports/full-cycle-2026-05-08-full-pass.md
Финальный счёт 10 ✓ / 2 ✗ / 0 ⚠ / 0 ◯ — две ✗ это РЕАЛЬНЫЕ баги:
[HIGH] step11 oversell проходит /post (нужна валидация qty≤stock)
[MEDIUM] step08 Supply без supplierId → 500 вместо 400
2026-05-08 11:01:56 +05:00

8.3 KiB
Raw Blame History

E2E report: full-cycle

Запущен: 2026-05-08T05:33:45.050Z Длительность: 9.7с

Итог: 10 ✓ / 2 ✗ / 0 ⚠ / 0 ◯ (всего 12)

✓ Step step01_create_organization: SuperAdmin создаёт «Test Shop {timestamp}» (KZ, KZT, ФЛК телефона)

Длительность: 1316мс

Тип Проверка Результат
api POST /api/super-admin/organizations → 200 ✓ org=Test Shop 1778218425049
api GET /api/super-admin/organizations включает созданную org
api Невалидный phone отвергается ✓ 400

✓ Step step02_create_first_admin: SuperAdmin создаёт первого Admin сотрудника организации (Employee + AppUser)

Длительность: 619мс

Тип Проверка Результат
api Temp password возвращён CreateOrgResult ✓ len=12
db employees содержит ровно 1 запись для новой org ✓ count=1
db AspNetUserRoles содержит role=Admin для нового user ✓ Admin

✓ Step step03_login_as_admin: Логин под admin (не SuperAdmin override) — JWT с org_id и role=Admin

Длительность: 540мс

Тип Проверка Результат
api /connect/token password-grant выдал токен
api /api/me содержит role=Admin ✓ Admin
api /api/me содержит правильный orgId ✓ 13ed4954-afeb-47fe-8d4a-7070758c7158

✓ Step step04_create_storekeeper_and_cashier: Admin создаёт Storekeeper и Cashier через /settings/employees

Длительность: 1318мс

Тип Проверка Результат
api employee-roles list ✓ 200, total=3
api Системная роль «Кладовщик» существует
api Системная роль «Кассир» существует
api POST /api/organization/employees (Кладовщик) ✓ 200
api POST /api/organization/employees (Кассир) ✓ 200
db employees total = 3 (admin + keeper + cashier) ✓ count=3
api Невалидный email отвергается при createAccount ✓ 400

✓ Step step05_login_as_cashier: Логин под Cashier — role-guard проверяется (sidebar/role guard)

Длительность: 671мс

Тип Проверка Результат
api /api/me содержит роль соответствующую системной Cashier ✓ Cashier
api Cashier → GET /api/organization/employees → 403 ✓ 403
api Cashier → GET /api/sales/retail — доступен ✓ 200

✓ Step step06_create_counterparty: Admin создаёт «ТОО Тест Поставщик» (БИН + телефон)

Длительность: 182мс

Тип Проверка Результат
api POST /api/catalog/counterparties ✓ 201

✓ Step step07_ensure_main_store: Проверить что есть main store (из bootstrap), иначе создать

Длительность: 84мс

Тип Проверка Результат
api GET /api/catalog/stores ✓ 200
db Main store существует (от bootstrap) ✓ Основной склад

✗ Step step08_create_supply: Admin создаёт Supply Draft (3-5 товаров) и проводит (Posted)

Длительность: 2005мс

Тип Проверка Результат
api Создано 3 product (валидный barcode + price + group) ✓ e2e Product 1 1778218425049, e2e Product 2 1778218425049, e2e Product 3 1778218425049
api Supply без supplierId → 400/409 ✗ 500
api Supply с пустым lines[] → 400 ✓ 400
api POST /api/purchases/supplies (Draft) ✓ 201
api POST /api/purchases/supplies/{id}/post (Draft → Posted) ✓ 204
api Повторный post Supply → 409 (idempotency) ✓ 409 {"error":"Документ уже проведён."}
db stock_movements содержат запись на каждую строку Supply ✓ count=3, expected=3

✓ Step step09_check_stock_after_supply: GET /api/inventory/stock — quantity увеличился на supplied amount

Длительность: 878мс

Тип Проверка Результат
api stock(e2e Product 1 1778218425049) +10 (было 0, стало 10) ✓ delta=10, expected=10
api stock(e2e Product 2 1778218425049) +15 (было 0, стало 15) ✓ delta=15, expected=15
api stock(e2e Product 3 1778218425049) +20 (было 0, стало 20) ✓ delta=20, expected=20
api GET /api/inventory/stock без storeId возвращает строки на каждый склад ✓ rows=1, stores=07d23bb1
db stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 1 1778218425049… ✓ sum_movements=10 stocks.Quantity=10
db stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 2 1778218425049… ✓ sum_movements=15 stocks.Quantity=15
db stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 3 1778218425049… ✓ sum_movements=20 stocks.Quantity=20

✓ Step step10_ensure_retail_point: Проверить или создать розничную точку (кассу)

Длительность: 168мс

Тип Проверка Результат
api RetailPoint существует Касса 1
api RetailPoint с несуществующим storeId → 400/404 ✓ 400

✗ Step step11_create_retail_sale: Admin создаёт RetailSale, 2 позиции из приёмки, cash, Post

Длительность: 1035мс

Тип Проверка Результат
api Продажа qty>остатка → /post должен 4xx ✗ 204
api Продажа с отрицательным qty/price → 400 ✓ 400
api discount=10 на line(price=100,qty=1) → lineTotal=90 ✓ lineTotal=90
api POST /api/sales/retail (Draft) ✓ 201
api POST /retail/{id}/post ✓ 204
api Повторный post RetailSale → 409 ✓ 409

✓ Step step12_check_stock_after_sale: GET /api/inventory/stock — quantity уменьшился на sold amount

Длительность: 889мс

Тип Проверка Результат
api stock product=f94d281a… 2 (было 10, стало 8) ✓ delta=2, expected=2
api stock product=6e22649a… 2 (было 15, стало 13) ✓ delta=2, expected=2
db stock_movements запись на sale-line f94d281a… ✓ count=1, sum=-2 (expected sum=-2)
db stock_movements запись на sale-line 6e22649a… ✓ count=1, sum=-2 (expected sum=-2)
db stock_movements.Type = RetailSale (2) для sale документа ✓ types=2

Summary

  • Passed: 10
  • Failed: 2
  • Warnings: 0
  • Skipped: 0

Critical bugs

HIGH

  • [11] Розничная продажа провёл количество больше остатка
    • qty=99999 при остатке 10-15 — /post вернул 204 вместо 409/400.
    • Fix: RetailSalesController.Post должен валидировать sum(line.qty) ≤ stocks.Available.

MEDIUM

  • [08] Supply без supplierId → 500 вместо 400
    • Сервер бросил исключение (500) на отсутствующее required-поле supplierId. UI получит generic error вместо понятного field-level сообщения.
    • Fix: SupplyInput.SupplierId — [Required] / model validation; ловить FK-violation перед SaveChanges и возвращать 400 с описанием.

Logic gaps

  • Bootstrap новой org не создаёт дефолтную ProductGroup, поэтому ProductsController.Create падает с 400 «ProductGroupId required» пока юзер вручную не заведёт группу.
  • RetailSale Draft создаётся даже с пустым lines[]; ошибка появится только на /post — UX мог бы блокировать раньше.