food-market/docs/full-chain-e2e-progress.md
nns b6c4d6056d
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(e2e): хэши коммитов full-chain в прогресс-док
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:10:48 +05:00

215 lines
25 KiB
Markdown
Raw Permalink 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.

# 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
- [x] pg_dump stage → `~/backups/stage-before-reset-20261008-000016.dump` (17M, заголовок PGDMP ок)
- [x] scoped clean Демо через `deploy/stage-reset-demo.sql` (dry-run в транзакции → commit); удалено 100740 строк
- [x] 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 ✓
- [x] disk: удалён demo export-каталог; invoice StoredPath=NULL (снимки уже не на диске)
### Поправка 08.10 (ночь) — скриншоты каждого процесса ✅
- [x] Реальные Playwright-скриншоты каждого шага (вкл. 2а): форма/результат/документ/отчёт с числами
- [x] 1920×1080 @2x PNG (файлы 3840×2216), fullPage для отчётов; мобильные 390×844 @3x (инвент./фото-приёмка)
- [x] Имена `NN-шаг-что.png` в `reports/full-chain-<RUN_ID>/screenshots/`; B — всегда (~34 кадра), C — только при ✗
- [x] В отчёте каждый скрин рядом со строкой шага; `index.html` галерея (рус. подписи, превью→оригинал)
- [x] Без паролей/токенов (вход токеном в localStorage, поле пароля пустое); ключевые экраны — реальный браузер
### Шаг 2а (поправка 08.10) — сотрудники и точки продаж ✅
- [x] 2 точки (каждая своя касса + эквайринг), правка, деактивация/активация; смена на неактивной → отказ (400)
- [x] Сотрудники кассир/кладовщик/админ; правка ФИО/тел/роль; смена пароля (новый работает, старый нет);
блокировка → ни token (invalid_grant), ни refresh (400)
- [x] Привязка кассира: на точке 1 (закреплён) → успех; на точке 2 → отказ (retail-point-not-assigned);
две смены на одной точке → 409; параллельные смены т.1/т.2 не смешивают деньги (свои «должно быть»)
- [x] Права API(403: /money/accounts,/transactions,/employees) + UI (в меню кассира нет модуля «Деньги»/
«Настроек», есть только своя касса; кладовщик без денег); чужую орг никто (изоляция)
- [x] Отчёты по точкам (register) и кассирам (cashier): сумма = общей выручке дня (3300=3300)
- [x] В Демо (B): новые с меткой «(e2e)»/`fc-*`, в конце деактивированы (не удалены)
### Сценарий ✅ (все 16 шагов зелёные; C — temp-org, B — Демо на реальных товарах)
1. [x] Регистрация → вход → простой режим; скрытые модули 403 (demands/transfers/promo/abc)
2. [x] Группа, 5 товаров (разные ед.изм., 1 весовой 12.5) + цены + штрихкоды; поставщик (БИН+тел); права кассира/кладовщика
3. [x] Касса(Cash,50000)+банк(Bank,номер,200000)+эквайринг(Acquiring); balances = начальным
4. [x] Долг через корректировку WeOwe 100000 → долг=100000
5. [x] Приёмка №1 (кладовщик) → остатки +кол-во ровно; себестоимость=цене приёмки
6. [x] Приёмка №2 из табличной накладной (CSV по шаблону) → «Поставщик не указан» → провести; остатки
7. [x] Приёмка №3 по фото (OCR Claude CLI, до 3 попыток) → ручное сопоставление → провести; снимки с диска удалены (404)
8. [x] Оплата №1 частично нал из кассы, №2 полностью с банка; PayedSum; долг=начальный+приёмки−оплаты (точно)
9. [x] Смена: размен 5000, 3 продажи (нал/карта/смеш), oversell→409, возврат по чеку, внесение/выплата,
закрытие с расхождением 500 → expectedCash/difference + Z-отчёт; продажа без смены→409
10. [x] Оприходование +8, списание −2, инвентаризация CSV (часть): расхождение применено, непосчитанные НЕ обнулены
11. [x] Отчёты: продажи/прибыль(=выручка−cost)/остатки на дату/«Деньги»(=начальный+движения)/долги; по точкам/кассирам
12. [x] Инварианты: товар остаток≡Σдвижений; счёт≡начальный+Σопераций; долг≡формуле; изоляция (demo не видит temp)
13. [x] Отмена приёмки с проданным товаром→отказ; отмена платежа→долг вернулся (точно)
14. [x] Отчёт `reports/full-chain-<RUN_ID>.md` (ожид/факт/✓✗/длит + скрины) + галерея; в `~/nightly-verify.sh`
(04:00, C+UI) и `~/quality-watchdog.sh` step 11 (HR=03, api-only)
15. [x] 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)
4. **Кладовщик без прав на склад.** Роль «Кладовщик» не имела InventoryEdit/EnterEdit/LossEdit → API 403
на инвентаризацию/оприходование/списание, хотя веб их показывал (рассинхрон). Фикс: добавлены права в
дефолт роли (DevDataSeeder) + backfill существующих орг. Репро в шаге 10 (кладовщик→оприходование/списание не 403). ✅
5. **Инвентаризация обнуляла непосчитанное.** Create с пустыми строками авто-подтягивал ВСЕ товары склада с
ActualQty=0 (Diff=−stock) → проведение обнуляло всё непересчитанное (и всё вне выбранной группы). Фикс:
авто-подтянутые строки создаются как ActualQty=BookQty (Diff=0) → проведение их не трогает; кладовщик меняет
ActualQty только у пересчитанных (истинный ноль вводится явно). Репро в шаге 10 (пустая инвентаризация →
непосчитанный не обнулён). ✅ (без миграции/правок веба)
6. **Внутренний 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). ✅
7. **«Приёмка сканированием» не сохранялась.** SupplyScanPage читал валюты как массив
(`Array.isArray(r.data)`), а `/api/catalog/currencies` отдаёт PagedResult `{items}` → список валют пуст,
нет KZT по умолчанию. Фикс: `r.data.items`. ✅
8. **Поиск товара на экране накладной не работал.** InvoiceReviewPage слал `?q=`, а API ждёт `?search=`
(PagedRequest.Search). Фикс: `search`. ✅
9. **«Сегодня» считалось по 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 «коммит только по просьбе»). Готов закоммитить по команде.
- 08.10 — закоммичено и запушено в origin/main: `24646e0` auth · `aedcd3f` employees · `8785fd6` money · `839fcaf` inventory · `009a9d8` web · `fddf552` reports · `886cb92` e2e · `6f10896` deploy.