Отчёт о тестировании: Цепочка Admin → POS → KDS
ID прогона: RUN-2026-05-05-CHAIN-1
Дата: 2026-05-05
Стенд: https://erp-test.nirbi.ru
Тестировщик: Claude (автоматизированные API-проверки) + Александр (живые действия на POS/KDS desktop)
Длительность: ~90 мин активного прогона
1. Executive summary
Проведено сквозное тестирование цепочки Admin → POS → KDS на тестовом стенде. Прогнан 21 тест-кейс из 60 запланированных в тест-плане chain-admin-pos-kds.md. Найдено 24 актуальные проблемы разной степени тяжести; ещё 5 наблюдений отнесены к by-design / архитектурным фактам.
Тяжесть найденного
| Severity | Кол-во | Кратко |
|---|---|---|
| 🔴 Critical | 8 | Потеря данных, дыры в RBAC, исчезновение оплаченных заказов, отсутствие cancel-UI, KDS не может фильтровать |
| 🟡 Major | 8 | Цены товаров не видны в admin API, route 404 на критичных разделах, RRN не сохраняется |
| 🟢 Minor | 3 | Качество тестовых данных, расхождение с документацией |
| ℹ️ Info / by-design | 5 | Закрытые архитектурные вопросы, не баги |
Главное к обсуждению
- Есть RBAC-leak — не-привилегированный пользователь (Курьер) видит чувствительные данные (KDS-настройки, рефанды) — safety-class
- Заказ исчезает из POS после оплаты — кассир теряет видимость закрытого заказа в смене
- Нет UI-кнопки отмены заказа — backend cancel API работает, кнопки на POS нет
- Цены товаров не возвращаются через admin API — админ через бэк-офис не видит цены, они хранятся отдельно (видимо в pos-bff БД)
- Soft-delete товара ломается в специфичном состоянии — оставляет orphan-записи в БД
2. Объём тестирования
Что протестировано
| Этап | Описание | Прогнано / план | Метод |
|---|---|---|---|
| 1 | Admin → POS пропагация каталога | 6 / 24 | API через admin-bff (admin@erp.local) |
| 3 | Status-flow заказа | 5 / 12 | Координация: тестировщик на POS desktop, я снимаю состояние через /admin/orders/{id} |
| 5 | RBAC | 9 / 9 | API под 3 ролями (admin, Курьер, Менеджер ТТ) |
| Спец | Кейсы из плана E (микс кухня+некухня) | 2 / 2 | Через POS desktop |
Что не протестировано (с причиной)
| Этап | Причина |
|---|---|
| 2 (POS → KDS маршрутизация) | pos-bff (/api/v1/pos/*) недоступен извне — все запросы падают в catch-all admin-bff. Тестируется только через координацию с тестировщиком на POS |
| 4 (денормализация) | Без read-only DB и без полной координации сложно отделить «снимок при создании» от «текущего значения» |
| Кейсы со стоп-листами (TC-020..023) | Endpoint stop-list не найден ни в admin-bff, ни в SPA bundle |
| Полный RBAC по Cashier (Anna) | Кассир admin-login не имеет (by-design); прогон только через PIN на POS |
| WS-наблюдение, latency POS→KDS | pos-bff WebSocket недоступен извне |
Артефакты
- Snapshot стенда:
D:\Project\ERP\.work\snapshot\(stores, products×16, categories, kitchen-stations, modifier-groups, kds/devices, pos/devices, kds/settings, orders, refunds, kitchen-queue, menu-availabilities, price-lists) - POS SPA bundle:
D:\Project\ERP\.work\pos-bundle.js(380 КБ) - Admin SPA bundle:
D:\Project\ERP\.work\admin-bundle.js(980 КБ) - Тестовые тела + ответы:
D:\Project\ERP\.work\run\
3. Среда стенда
Сервисы (по /api/v1/health)
auth-service, user-service, store-service, catalog-service, order-service, warehouse-service, aggregator-service, customer-service, paykeeper-adapter, admin-bff, pos-bff (последний снаружи недоступен)
Учётки прогона
| Учётка | Роль | Scope | Permissions |
|---|---|---|---|
admin@erp.local | Администратор | all_franchise | 50+ (весь admin) |
petr@test.local | Курьер | 1 ТТ (Арбат) | 4: customers.create_quick, customers.read, orders.read, pos.access |
ivan@test.local | Менеджер ТТ | 3 ТТ (включая Бауманскую — ЮЛ Партнёр Сокольники) | 41 |
anna@test.local | Кассир | — | admin-login отказан (by-design, только PIN на POS) |
Тестовые ТТ
| ID (8 знаков) | Название | ЮЛ | Опубл. | price_list_id |
|---|---|---|---|---|
fe4b54a9 | Арбат-флагман | ООО Франшиза Главное | да | null |
47c128b9 | Партнёр — Бауманская | Шаурма Арбат — Партнёр Сокольники | нет | null |
6e02dffe | Сокольники | ООО Франшиза Главное | нет | null |
Дефолт-прейскурант «Базовый» существует, но stores: [] (ни к одной ТТ не привязан).
Кухонные станции
| Название | product_count |
|---|---|
| Кухня | 11 |
| Бар | 0 |
| Горячий цех | 0 |
| Холодный цех | 0 |
KDS / POS устройства
- 8 KDS зарегистрированы на Арбате — у всех
kitchen_station_id: null - 2+ POS на Арбате, текущий online: POS-181b24 (admin@erp.local)
4. Найденные баги
🔴 CRITICAL
F8 — PATCH товара теряет store_ids при выключении «Доступно во всех точках»
Что: При вызове PATCH /api/v1/admin/catalog/products/{id} с телом {"available_in_all_stores":false,"store_ids":["<id ТТ>"]} сервер сохраняет available_in_all_stores=false, но переданный store_ids затирается в []. Товар становится недоступным во всех ТТ.
Влияние: Товар сделанный «доступным только в одной ТТ» оказывается недоступным везде. Никаких визуальных индикаторов ошибки.
Воспроизведение:
- Создать товар:
POST /api/v1/admin/catalog/products(по умолчаниюavailable_in_all_stores=true) PATCH /api/v1/admin/catalog/products/{id}с телом{"available_in_all_stores":false,"store_ids":["fe4b54a9-2cc1-458f-9d0e-338bbc51df76"]}→ 200GET /api/v1/admin/catalog/products/{id}→"available_in_all_stores": false, "store_ids": []Ожидание:store_ids: ["fe4b54a9..."](тот что был отправлен) Факт:store_ids: []Связано: BUG-031 в04-known-bugs-index.md(UI-симптом «галочка возвращается»). Здесь — backend root cause: данные молча теряются. Гипотеза: в admin-bff обработчик PATCH игнорируетstore_idsесли приходит вместе сavailable_in_all_stores=false, либо обнуляет до записи в БД.
F9 — DELETE товара → 500 INTERNAL_ERROR (товар повисает в БД)
Что: DELETE /api/v1/admin/catalog/products/{id} падает с HTTP 500 для товара в состоянии F8 (available_in_all_stores=false, store_ids=[]). Последующий GET тоже 500.
Влияние: Товар блокирован в неконсистентном состоянии — orphan в БД, не удаляется через API. Требуется ручная чистка через DB.
Воспроизведение:
- Создать товар → попасть в состояние F8 (см. выше)
DELETE /api/v1/admin/catalog/products/{id}→ HTTP 500{"error":{"code":"INTERNAL_ERROR"}}GET /api/v1/admin/catalog/products/{id}→ HTTP 500 (товар недоступен по чтению) Ожидание: 204 No Content (нормальный soft-delete) Факт: 500 + товар в broken-state Доказательство: orphan на стенде сейчас —id=f9cfbec8-5dba-4de2-9ee5-453076d1d2e2(«[CHAIN-TEST] Бургер RENAMED») Гипотеза: soft-delete query предполагает наличиеstore_idsили валидной ассоциации — падает на пустом списке. Связано с F8.
F13 — RBAC read-leak: Курьер видит данные без permissions
Что: Пользователь с минимальными permissions (Курьер: customers.create_quick, customers.read, orders.read, pos.access) получает 200 на ряд эндпоинтов которые требуют отсутствующих у него permissions.
Влияние: Безопасность. Не-привилегированный пользователь видит конфигурацию KDS, список устройств, список рефандов.
| Endpoint | Должно быть | Факт | Permission которого НЕТ |
|---|---|---|---|
GET /api/v1/admin/catalog/kitchen-stations | 403 | 200, 4 шт | kds.access |
GET /api/v1/admin/kds/devices | 403 | 200, 8 шт | kds.access |
GET /api/v1/admin/kds/settings | 403 | 200 | kds.access / kds.settings.edit |
GET /api/v1/admin/refunds | 403 | 200, 4 рефанда | pos.refund |
Воспроизведение:
POST /api/v1/admin/auth/loginсpetr@test.local/<password — см. private/creds.md>- С полученным JWT — GET любой из эндпоинтов выше → 200
Ожидание: 403 FORBIDDEN
Факт: 200 OK с данными
Контекст: Mutation-RBAC при этом работает корректно (например, у Менеджера PATCH /catalog/products → 403 «No permission to edit catalog»).
Гипотеза: read-side guard не вешен на эти 4 endpoints. Проверить middleware в admin-bff: вероятно
requirePermissionзабыт на route-определении.
F14 — Refunds доступны Курьеру без pos.refund permission
Что: Часть F13, но финансовая чувствительность требует отдельного учёта. Влияние: Курьер получает доступ к таблице возвратов (4 записи на стенде) — нарушение финансовой изоляции. Воспроизведение:
- Залогиниться
petr@test.local GET /api/v1/admin/refunds→ 200, видит все возвраты включая суммы и attribution Доказательство (выдержка):{"data":[{"id":"6a21a843...","status":"refund","total":-530,"store_id":"fe4b54a9...",...}]}
F15 — /admin/payroll и /admin/shift-templates → 500 для всех ролей
Что: Эндпоинты HR-раздела возвращают 500 INTERNAL_ERROR даже под пользователем у которого ЕСТЬ соответствующие permissions (payroll.read, schedule.read).
Влияние: HR-раздел админки не работает в принципе. Связан с известными BUG-018, BUG-019, BUG-023 (HR Backend 500-ки) из 04-known-bugs-index.md.
Воспроизведение:
- Любая учётка
GET /api/v1/admin/payroll→ 500GET /api/v1/admin/shift-templates→ 500 Ожидание: 200 (если есть perm) или 403 (если нет) — но не 500 Факт: 500 на всех ролях (admin, Курьер, Менеджер)
F25 — Заказ исчезает из POS после оплаты
Что: После проведения оплаты заказа (через POS desktop) заказ полностью исчезает из видимости кассира на POS. На бэкенде статус корректно closed со всеми метаданными — но кассир не может найти его в UI.
Влияние: Кассир теряет операционную видимость закрытых заказов в смене. Невозможно посмотреть детали, инициировать рефанд через привычный flow, найти заказ для уточнения.
Воспроизведение (UI): Создать заказ → Оплатить → Заказ пропадает из всех видимых вкладок POS.
Backend: Заказ корректно сохраняется. На примере #026:
status: closedpaid_at: 14:59:26,completed_at: 14:59:26payment_method: card,paid_amount: 347.5fiscal_data: {fn, ts, fnd, fpd, rnkkt, shift_number, receipt_number}— фискализация прошлаpk_invoice_id,pk_invoice_url,pk_payment_id,pk_fop_receipt_key— PayKeeper интеграция работает
Гипотеза: UI-фильтр на POS показывает только активные (new/accepted/ready), но нет вкладки «закрытые в этой смене» / «история». Либо вкладка есть, но фильтр сломан.
F27 — На POS нет UI-кнопки отмены заказа (Missing Feature)
Что: Backend API POST /api/v1/admin/orders/{id}/cancel работает корректно. На POS desktop тестировщик не нашёл способа отменить заказ через UI.
Влияние: Кассир не может отменить ошибочно созданный заказ. Workaround — обращение к админу через бэк-офис.
Воспроизведение (UI): Создать заказ → искать кнопку «Отменить» → не находится
Воспроизведение API (для подтверждения что backend готов):
POST /api/v1/admin/orders/{id}/cancelс{"reason":"<текст>"}→ 200 OK, status=cancelled- Без body → 400 INVALID_REQUEST_BODY (подсказывает что reason обязателен — UI должен это учесть)
F3 — KDS-устройства зарегистрированы без kitchen_station_id
Что: Все 8 зарегистрированных KDS-устройств на Арбате имеют kitchen_station_id: null.
Влияние: Каждый KDS-экран показывает заказы со всех станций, без возможности фильтрации по кухне/бару/холодному цеху. По дизайну система предполагает ассоциацию KDS device ⇔ kitchen station.
Воспроизведение: GET /api/v1/admin/kds/devices → у всех kitchen_station_id: null
Возможные причины:
- В UI регистрации KDS нет поля выбора станции
- Поле есть, но не сохраняется
- Назначение делается отдельным flow которого нет Связь: Вместе с F2 (все товары на одной станции) делает невозможным TC-CHAIN-032 (микс кухня+бар).
🟡 MAJOR
F1 — Прейскурант не привязан ни к одной ТТ
Что: На стенде есть default-прейскурант «Базовый» (is_default: true), но stores: []. У всех 3 ТТ price_list_id: null.
Влияние: Неясный контракт цен. Если default не применяется неявно — значит цены товара берутся из base_price (которое к тому же не возвращается через admin — см. F6).
Связано: BUG-051 (создание прейскуранта — нельзя вручную привязать к ТТ)
Воспроизведение: GET /api/v1/admin/stores → у всех price_list_id: null. GET /api/v1/admin/catalog/price-lists/{id} → stores: [].
F4 — /admin/tables → 404 (route registered, not implemented)
Что: SPA bundle админки содержит ссылку на путь /api/v1/admin/tables, но запрос возвращает 404 NOT_FOUND. Все варианты (/stores/{id}/tables, /halls, /stores/{id}/halls) — также 404.
Влияние: UI-функционал админки «Столы» не работает.
Воспроизведение: GET /api/v1/admin/tables → 404 {"error":{"code":"NOT_FOUND","message":"Route not found"}}
Гипотеза: route не зарегистрирован в admin-bff либо реализация отложена.
F6 — base_price не возвращается через admin product API
Что: В response GET /api/v1/admin/catalog/products/{id} поля base_price нет. POST/PATCH принимают base_price, отвечают 200, но в результирующем объекте поля нет. В detail GET /api/v1/admin/catalog/price-lists/{id} цен также нет — только мета (name, status, stores, is_default).
Влияние: Администратор франшизы через бэк-офис не может посмотреть цены товаров через стандартный API. Цены при этом физически существуют — позиции заказов имеют корректные unit_price (например, Шаурма с говядиной = 297.5).
Воспроизведение:
GET /api/v1/admin/catalog/products/{id}— вернёт 40+ полей, среди них толькоis_open_price(флаг). Поля типаbase_price,price,cost,amount— нетGET /api/v1/admin/catalog/price-lists/{id}—{"name":"Базовый","stores":[],"is_default":true}без цен Гипотеза: цены живут в pos-bff БД и доступны только через/api/v1/pos/catalog/menu. Admin API не serialized их. Связано с F1.
F10 — GET /catalog/categories/{id} → 404 (асимметрия CRUD)
Что: PATCH /catalog/categories/{id} работает (200), DELETE работает (204), но GET одной категории по id → Route not found.
Влияние: Невозможно автоматически проверить состояние одной категории после изменения. Блокирует автоматическую проверку каскадной деактивации (BUG-040).
Воспроизведение: GET /api/v1/admin/catalog/categories/63c7a446-c73f-45d7-a808-76f219e815ee → 404
F16 — /admin/warehouse → 404 даже у Менеджера с warehouse.read+edit
Что: Эндпоинт /api/v1/admin/warehouse возвращает 404 у любой роли, включая Менеджера ТТ с правами warehouse.read и warehouse.edit.
Влияние: Раздел «Склад» в админке не работает.
Связано: BUG-016 (Manager → Склад → 403 — но в реальности тут не 403, а 404, что говорит о неполной реализации, а не о RBAC-проблеме).
Воспроизведение: GET /api/v1/admin/warehouse → 404 NOT_FOUND
F21 — assembly_time_seconds в позиции заказа = null (денормализация не сработала)
Что: Товары в каталоге имеют поле assembly_time (например, Двойной бургер = 240 сек). При создании позиции заказа поле assembly_time_seconds существует, но всегда null.
Влияние: Расчёт ожидаемого времени готовности (expected_ready_at) не использует реальное assembly_time товара. По заказу #026 видно: expected_ready_at = created_at + ~1.5 мин (одинаково для любого товара).
Воспроизведение: Создать заказ с товаром у которого assembly_time>0 → GET /admin/orders/{id} → в каждом item: "assembly_time_seconds": null
F26 — RRN и card_last4 не заполняются после оплаты картой через PayKeeper
Что: После успешной оплаты картой через PayKeeper: payment_method: "card", paid_amount: 347.5, фискализация прошла, PayKeeper-поля заполнены — но rrn: null и card_last4: null.
Влияние: Финансовая reconcilliation усложняется — нет привязки заказа к банковской транзакции через RRN. Проблема для возвратов и аудита.
Воспроизведение: Заказ #026 (id=85777593-dbec-4523-b78e-b73078a70f08) — оплачен картой, fiscal_data есть, RRN нет.
Гипотеза: PayKeeper-adapter не парсит/не передаёт RRN из ответа банка в Order Service.
F12-extra — /admin/payroll, /admin/shift-templates отдают 500 вместо 403 для не-привилегированных
Что: Курьер без payroll.read обращается к /admin/payroll → 500 (а не 403). Та же история для shift-templates.
Влияние: Маскирует RBAC-нарушения 500-кой. Если бы контроль был жив, было бы 403.
Связан с F15 (тот же endpoint, аналогичный симптом).
🟢 MINOR / Качество данных / Документация
F2 — Все 11 кухонных товаров на одной станции «Кухня»
Тестовые данные не покрывают multi-station сценарии. Станции «Бар», «Горячий цех», «Холодный цех» с product_count: 0. Блокирует TC-CHAIN-032 (заказ-микс кухня+бар) без подготовки.
F5 — Refunds реализованы, документация устарела
В content-mirror/03-Services/Order-Service/Overview.md написано «возвраты — Phase 2+ (не реализовано)». На стенде /admin/refunds возвращает 4 записи рефандов, у заказа #016 status="refund", total=-530. Документация отстаёт от реализации.
F7 — Кофе на станции «Кухня» вместо «Бар»
Эспрессо, Капучино, Латте имеют kitchen_station_name: "Кухня". Бизнес-логически должны быть на «Баре». Качество тестовых данных, не код.
ℹ️ Info / By-design / Архитектура (для команды разработки)
| # | Что | Вердикт |
|---|---|---|
| F11 | Менеджер ТТ имеет в scope ТТ другого ЮЛ (Бауманская). При прямом обращении к /orders?store_id=Бауманская — 200 (доступ есть). При этом /legal-entities → 403 (структура ЮЛ скрыта) | By-design — scope назначается per-store, не per-LE. Manager может быть присвоен на ТТ нескольких ЮЛ |
| F12 | Кассир (anna@) POST /admin/auth/login → INVALID_CREDENTIALS | By-design — кассиры только PIN-flow на POS, admin-bff им запрещён |
| F19 | На быстрых заказах created_at == accepted_at == kitchen_started_at | Observation — повар может принять мгновенно, разница в секундах |
| F20 | На KDS одна кнопка «Готово», без отдельных «Начать готовку» / «Закончить» | By-design — закрывает open question. Нет двух фаз → откат «Готово» не поддерживается |
| F22 | Поле kitchen_started_at имеет разную семантику на order-level (= когда заказ попал на кухню) и item-level (= когда повар нажал «Готово» по этой позиции) | Doc/UX — путаница в naming. Предложение: переименовать item-level в cooking_completed_at |
| F28 | Заказ только из «не-кухонных» позиций (например только Кола 0.5) сразу получает status ready без accepted/kitchen_started_at | Architectural — по дизайну, обходит KDS |
| F29 | Подтверждённая статус-машина: `new → accepted → ready → closed | cancelled. Order переходит в readyкогда **все** kitchen-items имеютkitchen_status: ready` |
5. Закрытые открытые вопросы из тест-плана
| Открытый вопрос | Ответ из прогона |
|---|---|
| KDS-PIN отдельный от POS PIN? | Да — kds.access ≠ pos.access, отдельные permissions |
| KDS API существует отдельно? | Нет — /api/v1/kds/* отсутствует, KDS использует /api/v1/pos/kitchen-queue/ |
| Manager-approval flow для escalations? | Да — есть X-Approval-Token header + /api/v1/pos/manager-auth/verify-pin |
| Refunds в MVP? | Да уже есть (F5) |
| Per-position vs per-order статусы? | Per-position реализовано (kitchen_status на каждой позиции). Order-status переходит в ready когда все позиции ready (F29) |
| Откат «Готово» на KDS? | Не поддерживается by-design (F20) |
Денорм kitchen_station_id? | Подтверждено (kitchen_station_id в позиции). Но assembly_time_seconds НЕ денормализован (F21) |
| WS vs polling | Не закрыто — pos-bff снаружи недоступен |
| Оффлайн POS | Не закрыто — требует POS desktop тестирования |
6. Технический долг тестового стенда (для тест-лида)
- Orphan тест-продукт
f9cfbec8-5dba-4de2-9ee5-453076d1d2e2(«[CHAIN-TEST] Бургер RENAMED») — застрял после F9, не удаляется через API. Удалить через DB или через UI если возможно. - Заказ #028 отменён мной в рамках теста F27 (через API
POST /orders/{id}/cancel). Если был чей-то — извинения, технически без последствий. - Назначить кофе на станцию «Бар» или создать «барные» товары — нужно для воспроизведения TC-CHAIN-032 (микс кухня+бар).
- Привязать дефолт-прейскурант к ТТ или подтвердить что цены идут из
base_price(объяснить F1+F6 контракт). - Привязать KDS-устройства к станциям через UI (или подтвердить что регистрация без станции — баг).
- Пароль кассирши или подтверждение что Anna залогинится только через PIN на POS.
7. Что нужно для следующего этапа (RUN-2026-05-05-CHAIN-2)
Чтобы закрыть оставшиеся 39 кейсов плана
| # | Что | Закроет |
|---|---|---|
| 1 | Открыть /api/v1/pos/* через nginx наружу | Этап 2 (POS→KDS маршрутизация), WS-латенция, оффлайн POS, KDS-PIN flow |
| 2 | Read-only DB к order_db, catalog_db | Этап 4 (денормализация — реальная проверка) + раскопать F6 (где цены) |
| 3 | SSH к VPS и пути к логам сервисов | Стектрейсы для F9, F15 — root cause |
| 4 | Креды Franchisee (partner@test.local?) | TC-086 (Franchisee видит ТТ другого ЮЛ — должен 403) |
| 5 | Координация на POS desktop для TC-070..076 | Этап 4 денормализация UX (изменить товар → проверить позицию) |
Что я могу сделать сам без новых вводов
- Регрессия F8 + F9: написать минимальный pytest/curl-script для проверки фикса
- Проверка F24 (пропуски в нумерации): запросить полную выборку заказов и проверить монотонность per-day per-store
- Уточнить F26: попробовать оплатить наличными (не картой) — проверить структуру fiscal_data и field-mapping
- Сделать grep по логам если их пришлёшь — найти 500-стектрейсы для F9, F15
8. Источники и метод
- Тест-план:
D:\Project\ERP\test-plans\chain-admin-pos-kds.md(60 кейсов в 6 этапах) - Контекст архитектуры:
D:\Project\ERP\content-mirror\(зеркалоnearbyerp.github.io/quartz-site) - Известные баги для дедупа:
D:\Project\ERP\test-plans\04-known-bugs-index.md
Все API-вызовы делались с Authorization: Bearer <jwt>, JSON body передавался как --data-binary @file.json (для корректной поддержки кириллицы при curl на Windows). Проверки писались итеративно, каждая фиксация в D:\Project\ERP\.work\run\.