food-market/docs/full-chain-e2e-progress.md
nns 886cb92f30 test(e2e): сквозной сценарий full-chain «жизнь магазина» + харнес со скриншотами
16 шагов против настоящего стенда: регистрация→справочники→сотрудники/точки→
деньги→3 приёмки→оплаты→смена/продажи/возврат/Z→склад→отчёты→инварианты→откат.
Сверяет конкретные числа и инварианты, 0×5xx/0×тостов/0×console.error. Режимы
C (temp-орг) и B (Демо на реальном каталоге). Hi-res скриншоты каждого процесса +
галерея. Включён в nightly-verify и quality-watchdog (скрипты в $HOME).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:10:11 +05:00

24 KiB
Raw Blame History

Full-chain E2E — прогресс

Автономная задача владельца (07.10.2026): один запускаемый сценарий «жизнь магазина от регистрации до отчётов», проходящий ВСЕ процессы простого режима через настоящий стенд https://test.admin.food-market.kz, сверяющий ЧИСЛА и инварианты, печатающий отчёт; повторяемый и включённый в ночной прогон.

Поправка владельца (07.10, применяется сразу):

  • A. Шаг 0 — резервная копия stage-БД, затем очистка «Демо-магазина» от всего кроме каталога/сотрудников/точек продаж.
  • B. Полная имитация дня (пп.1–13) ОДИН раз «как в жизни» прямо в «Демо-магазине» на настоящих товарах (вход demo-admin/keeper/cashier, регистрацию пропустить).
  • C. Повторяемый ночной тест — свежая временная организация каждый прогон, «Демо» не трогает.

Топология (выяснено 07.10)

  • Стенд test.admin.food-market.kz крутится на prod-vm 192.168.1.190.
    • DB: контейнер food-market-stage-postgres (127.0.0.1:5435 на prod-vm), db/user food_market.
    • API: food-market-stage-api (порт 8085), web: food-market-stage-web.
    • Доступ: ssh nns@192.168.1.190 "docker exec -i food-market-stage-postgres psql -U food_market -d food_market ...".
    • uploads на диске: /opt/food-market-stage-data/uploads (prod-vm).
  • Я на dev-vm 192.168.1.192. Локальный контейнер food-market-postgres (127.0.0.1:5434) — это DEV-БД, НЕ стенд. Поэтому lib/db.ts (docker exec food-market-postgres) для стенда НЕ годится. Решение: все инварианты в полном сценарии считаем через API; для Демо-очистки и ночной уборки ходим в stage-БД по ssh.
  • Деплой фикса на стенд: ~/deploy-stage.sh (билд на dev-vm → push в registry 192.168.1.193:5001 → ssh prod-vm docker compose -p food-market-stage pull/up). Есть guard stage↔prod isolation.

Орги на стенде

  • «Демо-магазин» = b7c403fe-7fc8-48af-9490-4a9a2eafe2c2 — 30894 товара (ЦЕЛЬ очистки A/B).
  • «Demo Market» = f406452c-3d33-4c1d-84bd-e1d445a132f7 — 50 товаров, НЕ трогать.
  • Всего 453 орги (много autotest-мусора Reg-*, чистится prune-логикой).

Классификация таблиц для очистки Демо (A)

KEEP (каталог + сотрудники + точки продаж + reference): products, product_prices, product_barcodes, product_groups, product_images, org_units_of_measure, price_types, employees, employee_roles, users, roles, stores, retail_points, employee_retail_point_assignments, expense_items, user_presets.

DELETE (org-scoped, по OrganizationId, retry-loop для FK): supplies, supply_lines, enters, enter_lines, losses, loss_lines, inventories, inventory_lines, transfers, transfer_lines, demands, demand_lines, supplier_returns, supplier_return_lines, retail_sales, retail_sale_lines, cash_shifts, cash_drawer_operations, pos_batch_acks, stocks, stock_movements, money_accounts, money_transactions, money_transaction_links, counterparties, counterparty_adjustments, counterparty_tags, loyalty_cards, loyalty_programs, promotions, import_jobs, invoice_uploads, invoice_aliases, org_exports, org_audit_log. Плюс файлы invoice_uploads с диска /opt/food-market-stage-data/uploads.

Проверка после A: products=30894, и документы/остатки=0 (supplies/retail_sales/enters/losses/ inventories/transfers/demands/supplier_returns/stocks/stock_movements/money_/cash_=0).

Чек-лист

Шаг 0 (A) — backup + очистка Демо ✅ ВЫПОЛНЕНО 08.10

  • pg_dump stage → ~/backups/stage-before-reset-20261008-000016.dump (17M, заголовок PGDMP ок)
  • scoped clean Демо через deploy/stage-reset-demo.sql (dry-run в транзакции → commit); удалено 100740 строк
  • verify: products=30894 ✓, product_prices=30894, product_barcodes=30907, product_groups=53, employees=3, users=3 (demo-admin/keeper/cashier), stores=1, retail_points=2; supplies/retail_sales/enters/losses/inventories/stocks/stock_movements/money_/cash_/ counterparties/invoice_uploads/org_audit_log = 0 ✓
  • disk: удалён demo export-каталог; invoice StoredPath=NULL (снимки уже не на диске)

Поправка 08.10 (ночь) — скриншоты каждого процесса ✅

  • Реальные Playwright-скриншоты каждого шага (вкл. 2а): форма/результат/документ/отчёт с числами
  • 1920×1080 @2x PNG (файлы 3840×2216), fullPage для отчётов; мобильные 390×844 @3x (инвент./фото-приёмка)
  • Имена NN-шаг-что.png в reports/full-chain-<RUN_ID>/screenshots/; B — всегда (~34 кадра), C — только при ✗
  • В отчёте каждый скрин рядом со строкой шага; index.html галерея (рус. подписи, превью→оригинал)
  • Без паролей/токенов (вход токеном в localStorage, поле пароля пустое); ключевые экраны — реальный браузер

Шаг 2а (поправка 08.10) — сотрудники и точки продаж ✅

  • 2 точки (каждая своя касса + эквайринг), правка, деактивация/активация; смена на неактивной → отказ (400)
  • Сотрудники кассир/кладовщик/админ; правка ФИО/тел/роль; смена пароля (новый работает, старый нет); блокировка → ни token (invalid_grant), ни refresh (400)
  • Привязка кассира: на точке 1 (закреплён) → успех; на точке 2 → отказ (retail-point-not-assigned); две смены на одной точке → 409; параллельные смены т.1/т.2 не смешивают деньги (свои «должно быть»)
  • Права API(403: /money/accounts,/transactions,/employees) + UI (в меню кассира нет модуля «Деньги»/ «Настроек», есть только своя касса; кладовщик без денег); чужую орг никто (изоляция)
  • Отчёты по точкам (register) и кассирам (cashier): сумма = общей выручке дня (3300=3300)
  • В Демо (B): новые с меткой «(e2e)»/fc-*, в конце деактивированы (не удалены)

Сценарий ✅ (все 16 шагов зелёные; C — temp-org, B — Демо на реальных товарах)

  1. Регистрация → вход → простой режим; скрытые модули 403 (demands/transfers/promo/abc)
  2. Группа, 5 товаров (разные ед.изм., 1 весовой 12.5) + цены + штрихкоды; поставщик (БИН+тел); права кассира/кладовщика
  3. Касса(Cash,50000)+банк(Bank,номер,200000)+эквайринг(Acquiring); balances = начальным
  4. Долг через корректировку WeOwe 100000 → долг=100000
  5. Приёмка №1 (кладовщик) → остатки +кол-во ровно; себестоимость=цене приёмки
  6. Приёмка №2 из табличной накладной (CSV по шаблону) → «Поставщик не указан» → провести; остатки
  7. Приёмка №3 по фото (OCR Claude CLI, до 3 попыток) → ручное сопоставление → провести; снимки с диска удалены (404)
  8. Оплата №1 частично нал из кассы, №2 полностью с банка; PayedSum; долг=начальный+приёмки−оплаты (точно)
  9. Смена: размен 5000, 3 продажи (нал/карта/смеш), oversell→409, возврат по чеку, внесение/выплата, закрытие с расхождением 500 → expectedCash/difference + Z-отчёт; продажа без смены→409
  10. Оприходование +8, списание −2, инвентаризация CSV (часть): расхождение применено, непосчитанные НЕ обнулены
  11. Отчёты: продажи/прибыль(=выручка−cost)/остатки на дату/«Деньги»(=начальный+движения)/долги; по точкам/кассирам
  12. Инварианты: товар остаток≡Σдвижений; счёт≡начальный+Σопераций; долг≡формуле; изоляция (demo не видит temp)
  13. Отмена приёмки с проданным товаром→отказ; отмена платежа→долг вернулся (точно)
  14. Отчёт reports/full-chain-<RUN_ID>.md (ожид/факт/✓✗/длит + скрины) + галерея; в ~/nightly-verify.sh (04:00, C+UI) и ~/quality-watchdog.sh step 11 (HR=03, api-only)
  15. 3 бага найдено и починено (repro→fix→deploy→rerun зелёный); прогоны зелёные (см. ниже)

Найденные и исправленные баги

  1. EmployeesController.Update → 500 (DbUpdateConcurrencyException «0 rows affected») при правке сотрудника, у которого есть привязка к точке продаж. Причина: EF8 путается на nav-collection RemoveRange+Clear+Add (тот же баг, что чинили в Supplies/Enters/Losses/Demands.Update). Фикс: ExecuteDeleteAsync по EmployeeId + DbSet.Add (без Clear nav). Исправлено в коде, ждёт deploy. Воспроизведение: создать сотрудника с retailPointIds=[точка], затем PUT любой правки → было 500. Фикс в EmployeesController.Update. ✅ Задеплоено, прогон зелёный.
  2. Кассир видел модуль «Деньги» (GET /api/money/accounts → 200): роль «Кассир» имела MoneyView. Фикс: дефолт кассира MoneyView=false (DevDataSeeder) + backfill существующих орг (вкл. демо) в EnsureMoneyDefaultsAsync (снимает MoneyView, оставляет CashShiftOperate). Эндпоинты своей смены/Z (CashShiftsController List/Get/Report) → RequiresPermission("MoneyView,CashShiftOperate"); добавлена ИЛИ-семантика прав в PermissionRequirement/PermissionAuthorizationHandler. Веб уже корректно прячет модуль «Деньги»/«Настройки» у кассира (оставляет только «Смены»). ✅ Задеплоено, кассир→403 на деньги, 200 на свою смену.
  3. Смена открывалась на неактивной точке; кассир мог открыть смену на непривязанной точке. Фикс в MoneyService.OpenShiftAsync: проверка RetailPoint.IsActive (retail-point-inactive) и привязки кассира (retail-point-not-assigned; у кого привязок нет — на любой). ✅ Задеплоено, прогон зелёный.

Прочие найденные дефекты оказались в самом тесте (не в продукте): неуникальный EAN13-генератор, r.data.employee.id вместо r.data.id, groupBy register (а не retailPoint), пароль quick-login __QUICK_LOGIN_PWLESS__ (а не 123456), слишком строгая UI-проверка меню кассира (/money/shifts — его касса). Все поправлены.

Баги из тестирования iOS-приложения (поправка владельца 08.10)

  1. Кладовщик без прав на склад. Роль «Кладовщик» не имела InventoryEdit/EnterEdit/LossEdit → API 403 на инвентаризацию/оприходование/списание, хотя веб их показывал (рассинхрон). Фикс: добавлены права в дефолт роли (DevDataSeeder) + backfill существующих орг. Репро в шаге 10 (кладовщик→оприходование/списание не 403). ✅
  2. Инвентаризация обнуляла непосчитанное. Create с пустыми строками авто-подтягивал ВСЕ товары склада с ActualQty=0 (Diff=−stock) → проведение обнуляло всё непересчитанное (и всё вне выбранной группы). Фикс: авто-подтянутые строки создаются как ActualQty=BookQty (Diff=0) → проведение их не трогает; кладовщик меняет ActualQty только у пересчитанных (истинный ноль вводится явно). Репро в шаге 10 (пустая инвентаризация → непосчитанный не обнулён). ✅ (без миграции/правок веба)
  3. Внутренний EAN-13 с неверной контрольной цифрой. Веб-генератор (lib/barcode.ts digitsChecksum) считал веса слева, а для 12-значного тела чётность переворачивается. Фикс: веса справа налево (EAN-8/UPC-A не изменились). Уже выданные коды починены скриптом deploy/fix-internal-ean13.sql: исправлен 41 код (0 конфликтов, 0 осталось); список — docs/ean13-fixed-20261008.txt. Прод НЕ трогали (скрипт ручной, не auto-migration). ✅
  4. «Приёмка сканированием» не сохранялась. SupplyScanPage читал валюты как массив (Array.isArray(r.data)), а /api/catalog/currencies отдаёт PagedResult {items} → список валют пуст, нет KZT по умолчанию. Фикс: r.data.items. ✅
  5. Поиск товара на экране накладной не работал. InvoiceReviewPage слал ?q=, а API ждёт ?search= (PagedRequest.Search). Фикс: search. ✅
  6. «Сегодня» считалось по UTC, а не Asia/Almaty (UTC+5). Дашборд (RevenueToday/Week/Month + график), /api/reports/sales и /profit группировка period:day — все считали день по UTC. Фикс: хелпер BusinessTime (UTC+5) + применён в RetailSalesController /stats и report day-bucket'ах. ✅

API-карта (выверено по контроллерам, для сценария)

Base: E2E_ADMIN_URL=https://test.admin.food-market.kz. Логин: POST /connect/token (password grant, client_id=food-market-web). Демо-трио (режим B): username demo-admin|demo-keeper|demo-cashier@food-market.local, пароль-sentinel __QUICK_LOGIN_PWLESS__ (quick-login беспарольный вход на стенде; не 123456).

  • Регистрация (C): POST /api/auth/signup {email,password,organizationName,phone,plan:'start'} → 200; орг по умолчанию простой режим.
  • Простой режим: [RequiresAdvancedMode] → 403 code=advanced-mode-required. Закрыты: demands(/api/sales/demands), transfers(/api/inventory/transfers), promotions, loyalty, abc-report, telegram, audit-log, price-types мутации, user-presets.
  • Каталог: units /api/catalog/units-of-measure; groups /api/catalog/product-groups {name,parentId,sortOrder}; price-types /api/catalog/price-types (isRetail); currencies /api/catalog/currencies (KZT); products POST /api/catalog/products {name,article,unitOfMeasureId,vat,vatEnabled,productGroupId,isService,packaging, referencePrice,purchaseCurrencyId,prices:[{priceTypeId,amount,currencyId}],barcodes:[{code,type,isPrimary}]}.
  • Контрагент: POST /api/catalog/counterparties {name,legalName,type(0=ЮЛ),bin,countryId,...}.
  • Магазин/точки: /api/catalog/stores {name,code,isMain,isActive}; /api/catalog/retail-points {name,code,storeId,isActive}.
  • Сотрудники: /api/organization/employees POST {lastName,firstName,position,email,phone,roleId,isActive,retailPointIds,createAccount} → generatedPassword; PUT/DELETE требуют роли Admin; роли /api/organization/employee-roles (системные «Кассир»,«Кладовщик»,Admin).
  • Деньги (perm MoneyView/MoneyEdit/CashShiftOperate):
    • Счета POST /api/money/accounts {Name,Kind(Cash=1,Safe=2,Bank=3,Acquiring=4),BankName?,AccountNumber?,StoreId?(обяз.для Cash),CurrencyId,OpeningBalance,OpeningDate,IsActive}. Баланс = OpeningBalance + Σ(PaymentIn/CashIn/TransferIn) − Σ(PaymentOut/CashOut/TransferOut) по Posted. Report GET /api/money/reports/balances.
    • Корректировка долга POST /api/money/adjustments {Date,CounterpartyId,Direction(WeOwe=1,TheyOwe=2),Amount,Notes} + /{id}/post.
    • Транзакция POST /api/money/transactions {Date,Kind(PaymentIn=1,PaymentOut=2,CashIn=3,CashOut=4),AccountId,CounterpartyId?,ExpenseItemId?,CurrencyId,Amount,Links:[{SupplyId?,SupplierReturnId?,LinkedSum}]} + /{id}/post. Оплата приёмки = PaymentOut/CashOut с Links[{SupplyId,LinkedSum}] → поднимает supply.PayedSum (инвариант 0≤PayedSum≤Total).
    • Смена POST /api/money/shifts/open {RetailPointId,CashAccountId,AcquiringAccountId?,OpeningFloat}; /{id}/drawer {Direction(In=1,Out=2),Amount,Reason}; /{id}/close {ActualCash,Notes} → {expectedCash,difference}; /{id}/report (Z). 409 ShiftAlreadyOpen. ExpectedCash = OpeningFloat + salesCash − returnsCash + drawerIn − drawerOut; salesCash=Σ PaidCash non-return по CashShiftId; CashDifference = Actual − Expected.
  • Приёмка POST /api/purchases/supplies {date,supplierId,storeId,currencyId,notes,lines:[{productId,quantity,unitPrice}]} + /{id}/post (perm SuppliesPost). Повторный post → 409.
  • Инвойсы POST /api/purchases/invoices/upload (form files, xlsx/csv/pdf); /upload-photos (form files,codes); фон InvoiceWorker: pending→parsing→needs_review|failed; PUT /{id}/lines {Lines,SupplierName,...} (ручной матч ProductId+MatchedBy=manual); POST /{id}/apply {SupplierId?(null→«Поставщик не указан»),StoreId,CurrencyId,Notes} → создаёт Supply Draft; снимки с диска удаляются.
  • Продажа POST /api/sales/retail {date,storeId,retailPointId,customerId,currencyId,payment(Cash=0,Card=1,Mixed=99),paidCash,paidCard,notes,lines:[{productId,quantity,unitPrice,discount,vatPercent}],isReturn,referenceSaleId}
    • /{id}/post (требует откр.смену на точке иначе 409 code=cash-shift-required; oversell→409; недоплата→400). Возврат: /{id}/create-return→draft(isReturn)→post. /{id}/unpost (при наличии проведённых возвратов→409).
  • Склад: enters POST /api/inventory/enters {date,storeId,currencyId,notes,lines:[{productId,quantity,unitCost}]}+post (оприходование, +qty); losses POST /api/inventory/losses {date,storeId,currencyId,reason,notes,lines:[{productId,quantity,unitCost}]}+post (−qty); инвентаризация POST /api/inventory/inventories/import (form file csv/xlsx: «Штрихкод»/«Количество»→{found,unknown}); POST /api/inventory/inventories {date,storeId,notes,lines:[{productId,actualQty}]}(частичная=только эти товары)+post (двигает только Diff≠0, непосчитанные не трогаются). stock GET /api/inventory/stock?productId&storeId; movements GET /api/inventory/movements.
  • Отчёты: sales GET /api/reports/sales?from&to&groupBy=period:day|retailPoint|cashier|product&storeId →[{Key,Label,Revenue,Discount,Transactions,Quantity}]; profit GET /api/reports/profit?from&to →[{..,Revenue,Cost,Profit,MarginPercent}]; stock GET /api/reports/stock?date&storeId&includeZero →[{..,Quantity,Cost,Value}] (остатки на дату); деньги balances/movement/supplier-debts (долг = (Σприёмки−Σоплачено)−(Σвозвраты−Σполучено)+(WeOwe−TheyOwe)).

Фикстура фото-накладной: tests/fixtures/invoice-sample.jpg.

Журнал

  • 07.10 — старт; выяснена топология стенда; классификация таблиц; план зафиксирован.
  • 08.10 — Шаг 0 (A) выполнен (backup+clean Демо). Выверена полная API-карта по контроллерам. Приступаю к сценарию.
  • 08.10 — сценарий зелёный (16/16); 3 бага (EF8 nav, кассир-MoneyView, привязка/неактивная точка) + 2а строгие проверки; скриншоты+галерея; B-прогон в Демо зелёный; в nightly+watchdog. 2 зелёных прогона C подряд.
  • 08.10 — iOS-cleanup Демо: транзакционные ios-документы удалены ещё pre-B reset'ом (reset-demo.sql: приёмки/ оприходования/списания/инвентаризации/контрагенты в DELETE-set). Остаточный ios-товар «Тестовый товар (ios-тест)» (83b8254c, создан после reset) + его 3 штрихкода + 1 цена — удалены. Реальных правок цены после reset не найдено (ios-правка цены была на этом удалённом товаре). «Gerlax D2 iOS» — реальный товар каталога (04.10), оставлен.
  • 08.10 — поправка: 6 багов из iOS-тестирования (см. список выше, пп.4–9) — репро в сценарии, фикс, деплой, EAN-13 скрипт (41 код). Все репро после деплоя зелёные (кладовщик 201; пустая инвентаризация: непосчитанное 35→35; EAN-13 валиден). Дашборд revenueToday по Алматы верен (10650, 6 чеков сегодня).
  • 08.10 — ✅ ФИНАЛ: B-showcase v2 на чистом Демо — 16/16 зелёный (36 скринов, галерея). Два прогона C подряд — оба 16/16 ЗЕЛЁНЫЙ. 9 багов (3 + 6) исправлены и задеплоены. touch ~/.fm-watchdog/DONE.

Статус: ГОТОВО ✅

  • Сценарий full-chain: 16/16 зелёный (режимы C и B). В ~/nightly-verify.sh (04:00, C+UI) и ~/quality-watchdog.sh step 11 (HR=03, api-only).
  • 9 исправленных багов (3 из разработки теста + 6 из iOS): все с репро в сценарии, задеплоены, повторный прогон зелёный.
  • Демо-магазин: backup сохранён; очищен (A); проведён показательный день (B) на реальном каталоге; e2e-сущности деактивированы; ios-мусор удалён.
  • Код-правки — в working tree (задеплоены на stage через ~/deploy-stage.sh), в git НЕ закоммичены (policy «коммит только по просьбе»). Готов закоммитить по команде.