BR 3.3 — Order Service
Источники
- BR 3.3 §R3/R4/R5/R6/R7
- Data Model
- Events
Задачи
Миграция БД
- Liquibase changeset
010-br-3-3-paykeeper-fields.xml:ALTER TABLE orders ADD COLUMN pk_invoice_id VARCHAR(50) NULL; ALTER TABLE orders ADD COLUMN pk_invoice_url TEXT NULL; ALTER TABLE orders ADD COLUMN pk_payment_id VARCHAR(50) NULL; ALTER TABLE orders ADD COLUMN pk_fop_receipt_key VARCHAR(100) NULL; ALTER TABLE orders ADD COLUMN fiscal_data JSONB NULL; ALTER TABLE orders ADD COLUMN fiscal_failed BOOLEAN NOT NULL DEFAULT false; CREATE UNIQUE INDEX uq_orders_pk_invoice_id ON orders(pk_invoice_id) WHERE pk_invoice_id IS NOT NULL; CREATE UNIQUE INDEX uq_orders_pk_payment_id ON orders(pk_payment_id) WHERE pk_payment_id IS NOT NULL;
Entity
-
Order.java— добавить поляpkInvoiceId,pkInvoiceUrl,pkPaymentId,pkFopReceiptKey,fiscalData(Map<String,Object> или типизированный DTO с@JdbcTypeCode(SqlTypes.JSON)),fiscalFailed
Producer — новые события
-
OrderPaymentRequestedProducer— эмититorder.payment_requestedкогда POS зовётPOST /api/v1/orders/{id}/request-payment(новый endpoint):- Собирает cart с fiscal-полями из Catalog (
vat_rate,payment_subject,payment_type) - Форматирует по спеке events
- Собирает cart с fiscal-полями из Catalog (
-
OrderRefundRequestedProducer— эмититorder.refund_requestedкогда админка зовётPOST /api/v1/admin/orders/{id}/refund- Использует
orders.pk_payment_idкак источник - Если
pk_payment_id IS NULL— legacy flow (не публикуем событие)
- Использует
Endpoint
-
POST /api/v1/orders/{id}/request-payment(POS BFF → Order Service):- Проверяет что заказ в валидном статусе
- Публикует
order.payment_requestedв Kafka - Возвращает 202 Accepted +
{order_id}(invoice_url придёт позже черезpaykeeper.invoice.created→ запись в БД)
-
POST /api/v1/admin/orders/{id}/refund— переработать:- Принимает
{amount, is_full_refund, refund_cart?, reason}+ permissionorders.refund - Если
pk_payment_id IS NOT NULL→ создаётRefundRecord status=started+ публикуетorder.refund_requested, возвращает 202 - Если
pk_payment_id IS NULL→ legacy (создаёт recorddoneсразу — без async flow), возвращает 201
- Принимает
Consumers — новые из PK
-
PaykeeperInvoiceCreatedConsumer(grouporder-service-pk-invoice):- Topic
paykeeper.invoice.created - Сохраняет
orders.pk_invoice_id,orders.pk_invoice_url
- Topic
-
PaykeeperPaymentReceivedConsumer(grouporder-service-pk-payment):- Topic
paykeeper.payment.received - Idempotency check: если
orders.pk_payment_id != null→ skip - Устанавливает:
paid_at,payment_method(маппинг поpayment_system_idPK:card=1/2/6 карты,qr=rsb sbp, etc.),paid_amount,pk_payment_id,card_last4 - Если
order.statusIN (new,accepted,ready) — эмититorder.paid(существующий, для Warehouse/Customer/Webhook)
- Topic
-
PaykeeperPaymentRefundedConsumer(grouporder-service-pk-refund):- Topic
paykeeper.payment.refunded - Находит
RefundRecordпоorder_id + status=started→status=done, заполняетpk_refund_id,fiscal_doc_number(если есть) - Если RefundRecord не найден (возврат инициирован извне через ЛК PK) — создаёт новую запись с
initiated_by=pk_external - Эмитит
order.refunded(существующий)
- Topic
-
PaykeeperReceiptFiscalizedConsumer(grouporder-service-pk-receipt):- Topic
paykeeper.receipt.fiscalized - Сохраняет
orders.fiscal_data = {fpd, fnd, fn, rnkkt, shift_number, receipt_number, ts},orders.pk_fop_receipt_key
- Topic
-
PaykeeperReceiptFailedConsumer(grouporder-service-pk-receipt-fail):- Topic
paykeeper.receipt.failed - Ставит
orders.fiscal_failed=true
- Topic
-
PaykeeperRefundFailedConsumer(grouporder-service-pk-refund-fail):- Topic
paykeeper.refund.failed - Ставит
RefundRecord.status=failed+error_message
- Topic
Admin endpoint для отображения
-
GET /api/v1/admin/orders/{id}— включить в responsefiscal_data,pk_fop_receipt_key,fiscal_failed, списокrefunds[]сstatus/amount/reason/initiated_by
Маппинг payment_system_id PK → наш payment_method
Подбираем по опыту (может уточниться после тестов):
- PK
ps_id=1(карты MasterCard) →card - PK
ps_id=2(Альфа-Банк) →card - PK
ps_id=6(Русский Стандарт) →card - PK
ps_id=40(Сбербанк) →card - SBP-провайдеры →
qr - Если неизвестно →
card(fallback)
Конфиг в application.yml как Map.
Тесты
- Unit: маппинг
ps_id → payment_method - Integration: publish
paykeeper.payment.received→ orders обновлены - Idempotency: повторная публикация — не ломает состояние
Критерии приёмки
- POS вызывает
POST /api/v1/orders/{id}/request-payment→ событие уходит в Kafka - После
paykeeper.payment.receivedзаказ в статусеpaid(сpaid_at,pk_payment_id) - Фискальные данные подтягиваются через
paykeeper.receipt.fiscalized - Возврат через админку:
POST /api/v1/admin/orders/{id}/refund→ создаётRefundRecord status=started→ послеpaykeeper.payment.refunded→done - Заказ без
pk_payment_id— refund работает legacy (сразуdoneбез внешнего вызова)