Отчёт о тестировании: Цепочка 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Кол-воКратко
🔴 Critical8Потеря данных, дыры в RBAC, исчезновение оплаченных заказов, отсутствие cancel-UI, KDS не может фильтровать
🟡 Major8Цены товаров не видны в admin API, route 404 на критичных разделах, RRN не сохраняется
🟢 Minor3Качество тестовых данных, расхождение с документацией
ℹ️ Info / by-design5Закрытые архитектурные вопросы, не баги

Главное к обсуждению

  1. Есть RBAC-leak — не-привилегированный пользователь (Курьер) видит чувствительные данные (KDS-настройки, рефанды) — safety-class
  2. Заказ исчезает из POS после оплаты — кассир теряет видимость закрытого заказа в смене
  3. Нет UI-кнопки отмены заказа — backend cancel API работает, кнопки на POS нет
  4. Цены товаров не возвращаются через admin API — админ через бэк-офис не видит цены, они хранятся отдельно (видимо в pos-bff БД)
  5. Soft-delete товара ломается в специфичном состоянии — оставляет orphan-записи в БД

2. Объём тестирования

Что протестировано

ЭтапОписаниеПрогнано / планМетод
1Admin → POS пропагация каталога6 / 24API через admin-bff (admin@erp.local)
3Status-flow заказа5 / 12Координация: тестировщик на POS desktop, я снимаю состояние через /admin/orders/{id}
5RBAC9 / 9API под 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→KDSpos-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 (последний снаружи недоступен)

Учётки прогона

УчёткаРольScopePermissions
admin@erp.localАдминистраторall_franchise50+ (весь 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 затирается в []. Товар становится недоступным во всех ТТ. Влияние: Товар сделанный «доступным только в одной ТТ» оказывается недоступным везде. Никаких визуальных индикаторов ошибки. Воспроизведение:

  1. Создать товар: POST /api/v1/admin/catalog/products (по умолчанию available_in_all_stores=true)
  2. PATCH /api/v1/admin/catalog/products/{id} с телом {"available_in_all_stores":false,"store_ids":["fe4b54a9-2cc1-458f-9d0e-338bbc51df76"]} → 200
  3. GET /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. Воспроизведение:

  1. Создать товар → попасть в состояние F8 (см. выше)
  2. DELETE /api/v1/admin/catalog/products/{id}HTTP 500 {"error":{"code":"INTERNAL_ERROR"}}
  3. 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-stations403200, 4 штkds.access
GET /api/v1/admin/kds/devices403200, 8 штkds.access
GET /api/v1/admin/kds/settings403200kds.access / kds.settings.edit
GET /api/v1/admin/refunds403200, 4 рефандаpos.refund

Воспроизведение:

  1. POST /api/v1/admin/auth/login с petr@test.local / <password — см. private/creds.md>
  2. С полученным 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 записи на стенде) — нарушение финансовой изоляции. Воспроизведение:

  1. Залогиниться petr@test.local
  2. 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. Воспроизведение:

  1. Любая учётка
  2. GET /api/v1/admin/payroll → 500
  3. GET /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: closed
  • paid_at: 14:59:26, completed_at: 14:59:26
  • payment_method: card, paid_amount: 347.5
  • fiscal_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 готов):

  1. POST /api/v1/admin/orders/{id}/cancel с {"reason":"<текст>"} → 200 OK, status=cancelled
  2. Без 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). Воспроизведение:

  1. GET /api/v1/admin/catalog/products/{id} — вернёт 40+ полей, среди них только is_open_price (флаг). Поля типа base_price, price, cost, amount — нет
  2. 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/loginINVALID_CREDENTIALSBy-design — кассиры только PIN-flow на POS, admin-bff им запрещён
F19На быстрых заказах created_at == accepted_at == kitchen_started_atObservation — повар может принять мгновенно, разница в секундах
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_atArchitectural — по дизайну, обходит KDS
F29Подтверждённая статус-машина: `new → accepted → ready → closedcancelled. Order переходит в readyкогда **все** kitchen-items имеютkitchen_status: ready`

5. Закрытые открытые вопросы из тест-плана

Открытый вопросОтвет из прогона
KDS-PIN отдельный от POS PIN?Даkds.accesspos.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. Технический долг тестового стенда (для тест-лида)

  1. Orphan тест-продукт f9cfbec8-5dba-4de2-9ee5-453076d1d2e2 («[CHAIN-TEST] Бургер RENAMED») — застрял после F9, не удаляется через API. Удалить через DB или через UI если возможно.
  2. Заказ #028 отменён мной в рамках теста F27 (через API POST /orders/{id}/cancel). Если был чей-то — извинения, технически без последствий.
  3. Назначить кофе на станцию «Бар» или создать «барные» товары — нужно для воспроизведения TC-CHAIN-032 (микс кухня+бар).
  4. Привязать дефолт-прейскурант к ТТ или подтвердить что цены идут из base_price (объяснить F1+F6 контракт).
  5. Привязать KDS-устройства к станциям через UI (или подтвердить что регистрация без станции — баг).
  6. Пароль кассирши или подтверждение что Anna залогинится только через PIN на POS.

7. Что нужно для следующего этапа (RUN-2026-05-05-CHAIN-2)

Чтобы закрыть оставшиеся 39 кейсов плана

#ЧтоЗакроет
1Открыть /api/v1/pos/* через nginx наружуЭтап 2 (POS→KDS маршрутизация), WS-латенция, оффлайн POS, KDS-PIN flow
2Read-only DB к order_db, catalog_dbЭтап 4 (денормализация — реальная проверка) + раскопать F6 (где цены)
3SSH к 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\.