Контракты до фикса не совпадали с реальными: - 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
8.3 KiB
8.3 KiB
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 мог бы блокировать раньше.