Desktop POS — Phase 3
Что сделано
Domain layer
packages/domain/src/types/customer.ts(new)Customer { id, phone, email, first_name, last_name, ... }QuickCreateCustomerRequest { phone, first_name?, last_name?, email? }customerDisplayName(c)— fallback на phone если имени нет
packages/domain/src/types/discount.ts(new)Discount { kind: "percent"|"fixed", value: number, approval_token? }calcDiscountAmount(d, baseTotal)— копейки, ≤ baseTotalapplyDiscount(d, baseTotal)— итог после скидки
packages/domain/src/types/order.ts—Order.customer_id,CheckoutRequest.customer_id
API client
packages/api-client/src/endpoints/customers.ts(new) — searchByPhone (404→null), quickCreate, attach/detachToOrder, getByIdpackages/api-client/src/endpoints/managerAuth.ts(new) — verifyPin(pin, scope) → ApprovalTokenpackages/api-client/src/endpoints/orders.ts:submit(req, extras?: SubmitExtras)— extras { discount_pct?, approval_token? }; bff body содержит customer_id+discount_pct, approval_token идёт через X-Approval-Tokenopen(req)— customer_id прокидывается
packages/api-client/src/endpoints/refunds.ts—create({ ..., approval_token? }); body перекладывается в плоский RefundBody под BFF (original_order_id,client_refund_id,amount,created_at)packages/api-client/src/bff-client.ts—RequestOptions.headersдля произвольных headers (X-Approval-Token); все методы (get/post/patch/del) принимают opts последним аргументом
Stores
stores/cartStore.ts:customer: Customer | null,discount: Discount | nullsetCustomer,setDiscountsubtotal()— без скидки,discountAmount()— копейки скидки,total()— после скидкиclear()сбрасывает customer + discount
stores/customerStore.ts(new) — кэшbyId: Record<string, Customer | null>,load(id)ленивая загрузка черезgetCustomersEndpoints().getById
Components (new)
CustomerAttachModal— поиск по телефону (debounce 400ms, минимум 10 цифр), показ найденного с кнопкой «Прикрепить» либо плейсхолдером «Не найден» + «Создать нового». Modesearch↔createформа (имя/фамилия/email).ManagerPinModal— переиспользуемый.scope({type, max_amount/pct}),reason(текст в красном bar),onApproved(token). NumericKeypad + 6 PIN dots.verifyPinчерез api-client.DiscountModal— toggle Процент/Сумма, инпут, live-preview «Сумма заказа / Скидка / Итог». Если discount > maxFreePct (20) — встроенный ManagerPinModal передonApply(token уходит в Discount.approval_token).
Screens
MainScreen:- Кнопки
+ Клиент/+ Скидка(рядом, после OrderTypeSwitcher); индикаторы заполнения; кнопка×для отвязки клиента - Bar
Без скидки vs −X ₽если скидка применена buildCheckoutItems/buildFiscalCartприменяютdiscountFactor()(= total/subtotal) пропорционально к unit_pricehandlePay:submit(req, { discount_pct, approval_token })+requestPaymentс customer_name/phone/email из cartStore.customerhandleSendToKitchen: customer_id передаётся в open- Скрыто в append-mode (нельзя менять клиента/скидку при дозаказе)
- Кнопки
RefundScreen:- Перед submit: если amt/100 > 1000 → ManagerPinModal с scope={refund_amount, max_amount}
- При onApproved → submitRefund(approval_token)
- Hint в форме: «Сумма превышает лимит — потребуется PIN менеджера»
OrderDetailScreen:- Если
order.customer_id— Meta «Клиент» через CustomerMeta (фетчит из customerStore.load) - Показывает
customerDisplayName + phone
- Если
Wiring
api/client.ts—getCustomersEndpoints(),getManagerAuthEndpoints()factories (mock/real)api/mockEndpoints.ts:- 3
mockCustomersс разными форматами phone createMockCustomersEndpoints— поиск через нормализацию\D→ digitscreateMockManagerAuthEndpoints— PIN9999→ token- mock
attach/detachToOrderобновляетorders.get(orderId).customer_id
- 3
Файлы
Created (8):
packages/domain/src/types/customer.tspackages/domain/src/types/discount.tspackages/api-client/src/endpoints/customers.tspackages/api-client/src/endpoints/managerAuth.tsapps/desktop/src/stores/customerStore.tsapps/desktop/src/components/CustomerAttachModal.tsxapps/desktop/src/components/ManagerPinModal.tsxapps/desktop/src/components/DiscountModal.tsx
Modified (10):
packages/domain/src/types/index.tspackages/domain/src/types/order.tspackages/api-client/src/index.tspackages/api-client/src/bff-client.tspackages/api-client/src/endpoints/orders.tspackages/api-client/src/endpoints/refunds.tsapps/desktop/src/api/client.tsapps/desktop/src/api/mockEndpoints.tsapps/desktop/src/stores/cartStore.tsapps/desktop/src/screens/MainScreen.tsxapps/desktop/src/screens/RefundScreen.tsxapps/desktop/src/screens/OrderDetailScreen.tsx
Тесты
pnpm typecheck— zero errorspnpm exec vite build— passing (196 modules, +8 от Phase 2)- Mock-mode E2E (browser):
- Прикрепить клиента +79161234567 → найден «Иван Петров» → Attached
- Применить скидку 15% → нет PIN → итог пересчитан
- Применить скидку 30% → ManagerPinModal → PIN 9999 → applied
- Refund 1500 ₽ → ManagerPinModal → PIN 9999 → submitted
Ссылки
- Order Service / POS BFF — соседние задачи
- Order Service API