BR 3.3 — Order Service

Источники

Задачи

Миграция БД

  • 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
  • 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} + permission orders.refund
    • Если pk_payment_id IS NOT NULL → создаёт RefundRecord status=started + публикует order.refund_requested, возвращает 202
    • Если pk_payment_id IS NULL → legacy (создаёт record done сразу — без async flow), возвращает 201

Consumers — новые из PK

  • PaykeeperInvoiceCreatedConsumer (group order-service-pk-invoice):
    • Topic paykeeper.invoice.created
    • Сохраняет orders.pk_invoice_id, orders.pk_invoice_url
  • PaykeeperPaymentReceivedConsumer (group order-service-pk-payment):
    • Topic paykeeper.payment.received
    • Idempotency check: если orders.pk_payment_id != null → skip
    • Устанавливает: paid_at, payment_method (маппинг по payment_system_id PK: card=1/2/6 карты, qr=rsb sbp, etc.), paid_amount, pk_payment_id, card_last4
    • Если order.status IN (new,accepted,ready) — эмитит order.paid (существующий, для Warehouse/Customer/Webhook)
  • PaykeeperPaymentRefundedConsumer (group order-service-pk-refund):
    • Topic paykeeper.payment.refunded
    • Находит RefundRecord по order_id + status=startedstatus=done, заполняет pk_refund_id, fiscal_doc_number (если есть)
    • Если RefundRecord не найден (возврат инициирован извне через ЛК PK) — создаёт новую запись с initiated_by=pk_external
    • Эмитит order.refunded (существующий)
  • PaykeeperReceiptFiscalizedConsumer (group order-service-pk-receipt):
    • Topic paykeeper.receipt.fiscalized
    • Сохраняет orders.fiscal_data = {fpd, fnd, fn, rnkkt, shift_number, receipt_number, ts}, orders.pk_fop_receipt_key
  • PaykeeperReceiptFailedConsumer (group order-service-pk-receipt-fail):
    • Topic paykeeper.receipt.failed
    • Ставит orders.fiscal_failed=true
  • PaykeeperRefundFailedConsumer (group order-service-pk-refund-fail):
    • Topic paykeeper.refund.failed
    • Ставит RefundRecord.status=failed + error_message

Admin endpoint для отображения

  • GET /api/v1/admin/orders/{id} — включить в response fiscal_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 заказ в статусе paidpaid_at, pk_payment_id)
  • Фискальные данные подтягиваются через paykeeper.receipt.fiscalized
  • Возврат через админку: POST /api/v1/admin/orders/{id}/refund → создаёт RefundRecord status=started → после paykeeper.payment.refundeddone
  • Заказ без pk_payment_id — refund работает legacy (сразу done без внешнего вызова)