Декомпозиция POS Phase 3 — Customer attach + Manager-PIN approval + Discounts
Источник
План
desktop-pos: «Точка управления заведением»— Phase 3. После Phase 1 (Tables) и Phase 2 (Append + Waiter).
Цель
- Прикрепление клиента к заказу по телефону (поиск + быстрое создание).
- Manager-PIN разблокирует крупные refund’ы (>1000 ₽) и большие скидки (>20%).
- Ручная скидка % или фикс-сумма с пересчётом фискального чека.
Затронутые сервисы / репозитории
| Слой | Репо | Задачи |
|---|---|---|
| desktop-pos | erp-pos-desktop | Desktop POS |
| POS BFF | erp-pos (bff/) | POS BFF |
| Order Service | erp-order-service | Order Service |
| Customer Service | erp-customer-service | без правок (всё уже было) |
| User Service | erp-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)
- ✅ Клиент по телефону: ищется через debounce, прикрепляется к корзине, передаётся в submit и виден в OrderDetailScreen.
- ✅ Refund > 1000 ₽: блокируется без X-Approval-Token, ввод PIN менеджера → пропускает.
- ✅ Discount > 20%: блокируется без token, ManagerPinModal встроен в DiscountModal.
- ✅ Approval token: HS256-подпись через JWT_SECRET, exp 5 мин, jti уникален.
- ⏸ 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
Ссылки
- POS Phase 1
- POS Phase 2
- Customer Service API (если есть)
- Order Service API