Декомпозиция POS Phase 3 — Customer attach + Manager-PIN approval + Discounts

Источник

План desktop-pos: «Точка управления заведением» — Phase 3. После Phase 1 (Tables) и Phase 2 (Append + Waiter).

Цель

  1. Прикрепление клиента к заказу по телефону (поиск + быстрое создание).
  2. Manager-PIN разблокирует крупные refund’ы (>1000 ₽) и большие скидки (>20%).
  3. Ручная скидка % или фикс-сумма с пересчётом фискального чека.

Затронутые сервисы / репозитории

СлойРепоЗадачи
desktop-poserp-pos-desktopDesktop POS
POS BFFerp-pos (bff/)POS BFF
Order Serviceerp-order-serviceOrder Service
Customer Serviceerp-customer-serviceбез правок (всё уже было)
User Serviceerp-user-serviceбез правок (validate-pin + listPOSEmployees уже были)

Backend ~70% уже было готово

Customer entity + InternalCustomerController (search-by-phone, quick-create) — BR 3.1. Order.customer_id колонка — миграция 006-br-3-1. User Service validatePin возвращает permissions. Phase 3 добивает: customer_id в CheckoutRequest/OpenDineInRequest, internal attach/detach в order-service, manager-auth route в BFF, approval-token middleware.

Decision points

  • JWT для approval — без зависимости jsonwebtoken: HS256 через node:crypto в bff/src/lib/approvalToken.ts. Подпись JWT_SECRET (тот же что и user-service для будущей унификации). TTL 5 мин, jti для будущей replay-защиты.
  • Discount хранение: на стороне POS, в Order Service не хранится отдельным полем. Применяется как пропорциональный коэффициент к unit_price каждой позиции — фискальный чек уходит со сниженными ценами. Минус — в админке “была скидка X%” не видно отдельно. Plus — нет миграций. Можно расширить до Order.discount_pct отдельной задачей если бизнесу нужна аналитика.
  • Manager permission: используем существующий pos.settings.edit (не вводим новый pos.refund.approve / pos.discount.approve чтобы не плодить permissions). Это та же роль что разрешает редактировать столы (Phase 1) — обычно один человек.
  • Approval scope: токен привязан к конкретному типу операции и порогу: { type: "refund_amount", max_amount } либо { type: "discount_pct", max_pct }. BFF проверяет: scope подходит И значение в запросе ≤ max_amount/max_pct.
  • Customer phone normalization: на стороне Customer Service есть PhoneNormalizer — POS отправляет любой формат, backend сам нормализует.

Acceptance criteria (PASS)

  1. ✅ Клиент по телефону: ищется через debounce, прикрепляется к корзине, передаётся в submit и виден в OrderDetailScreen.
  2. ✅ Refund > 1000 ₽: блокируется без X-Approval-Token, ввод PIN менеджера → пропускает.
  3. ✅ Discount > 20%: блокируется без token, ManagerPinModal встроен в DiscountModal.
  4. ✅ Approval token: HS256-подпись через JWT_SECRET, exp 5 мин, jti уникален.
  5. ⏸ Audit Kafka topic audit.pos.manager-approval — отложено (логирование в pos-bff log есть).

Прогресс

  • Order Service — customer_id в DTO + service apply + internal attach/detach
  • POS BFF — routes/customers.ts, routes/manager-auth.ts, lib/approvalToken.ts (HS256), middleware/auth.ts requireApprovalToken; orders.ts: customer_id + discount_pct + PATCH/DELETE customer; refunds.ts: amount > limit → 403 без token; config: jwtSecret, customerServiceUrl, лимиты
  • desktop-pos:
    • domain: Customer, Discount, Order.customer_id, CheckoutRequest.customer_id, calcDiscountAmount/applyDiscount
    • api-client: CustomersEndpoints (search/quick-create/attach/detach/getById), ManagerAuthEndpoints (verifyPin), orders.submit принимает SubmitExtras{discount_pct,approval_token}; refunds.create принимает approval_token; bff-client extra headers через RequestOptions
    • cartStore: customer + discount + setCustomer/setDiscount; subtotal/discountAmount/total; clear сбрасывает
    • customerStore — кэш по id для OrderDetailScreen
    • components: CustomerAttachModal (search 400ms debounce, create form), ManagerPinModal (NumericKeypad, scope, onApproved), DiscountModal (% или фикс, встроенный ManagerPinModal при >20%)
    • MainScreen: кнопки «+ Клиент» / «+ Скидка», bar «Без скидки vs со скидкой», передача customer_id + discount_pct + token в submit
    • RefundScreen: amount > 1000 ₽ → ManagerPinModal перед submit, токен в request
    • OrderDetailScreen: для customer_id показывает Meta «Клиент» (имя + телефон через customerStore)
    • mock: 3 customer seeds + manager-auth (PIN 9999) + customers attach/detach в orders

Verification

  • TypeScript desktop-pos: zero errors
  • Vite build: passing (196 modules, +8 от Phase 2)
  • POS BFF tsc: новые правки чисты (pre-existing catalog.ts ошибка не связана)
  • Java order-service: 4 файла изменены — собирается в Docker (Java 21)
  • VPS deploy: order-service + pos-bff rebuilt + healthy

Out of scope (отложено)

  • Phase 4: Aggregator inbox в POS, авто-снятие истёкших броней (cron в Java)
  • Phase 5: Tips Нетмонет, KDS by stations
  • Phase 6: Reports в POS, Printer config
  • Audit Kafka topic audit.pos.manager-approval — пока только log в pos-bff
  • Order.discount_pct отдельным полем — пока pseudo через unit_price factor

Ссылки