Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
25 KiB
Markdown
215 lines
25 KiB
Markdown
# 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.
|