Files
g2s-aggregator/spec/tasks/030_add_tbank_payment_notification_webhook.md
T

12 KiB
Raw Blame History

id, title, status, created
id title status created
030 Add TBank payment notification webhook and CDEK order creation TODO 2026-04-18

Context

TBank Init API уже получает NotificationURL, а данные заявки сохраняются в PostgreSQL после создания payment link. Сейчас приложение не принимает HTTP-уведомления TBank и не создаёт заказ в CDEK после подтверждения оплаты.

Goal

Добавить POST /api/v1/delivery/tbank/notifications, который принимает payment notification от TBank, проверяет token уведомления, при валидном статусе CONFIRMED находит сохранённую заявку по OrderId и регистрирует заказ в CDEK. Валидные уведомления с другими статусами должны подтверждаться без регистрации заказа в CDEK.

Constraints

  • Controller отвечает только за routing, DTO validation и HTTP error mapping; endpoint вызывает ровно один метод Service: AggregatorService.handle_tbank_payment_notification().
  • Service выполняет только orchestration: verification через TBank adapter, выбор действия через pure Business Logic, чтение/обновление заявки через OrderRepository и вызов injected CDEK order adapter.
  • Правило CONFIRMED -> создать заказ CDEK, остальные статусы -> подтвердить без CDEK должно быть pure Business Logic в app/domain/.
  • TBank-specific token verification должна быть инкапсулирована в app/adapters/tbank/ и использовать tbank_payment.auth.password.
  • Проверка token MUST следовать контракту TBank HTTP-уведомлений: использовать top-level scalar fields кроме Token, не включать вложенные объекты (Data, Receipt), добавить Password, отсортировать ключи по алфавиту, конкатенировать значения, посчитать SHA-256 и сравнить с Token.
  • Успешно обработанное уведомление MUST возвращать HTTP 200 с plain text body OK.
  • Валидные уведомления со статусами, отличными от CONFIRMED, MUST возвращать OK без чтения CDEK и без создания заказа.
  • Повторное валидное уведомление CONFIRMED для заявки с уже сохранённым cdek_order_uuid MUST возвращать OK без повторного вызова CDEK.
  • Если token невалиден, endpoint MUST возвращать deterministic 400 и не обращаться к repository или CDEK.
  • Если заявка для OrderId не найдена или CDEK registration не завершилась успешно, endpoint MUST не возвращать OK, чтобы TBank мог повторить notification delivery.
  • Repository содержит только CRUD/query/update primitives; без workflow logic и business decisions.
  • CDEK order payload MUST передавать order_uuid как external идентификатор заказа CDEK (поле number в CDEK order contract), чтобы повторные вызовы CDEK при HTTP timeout/ретрае обрабатывались идемпотентно на стороне CDEK и никогда не создавали дубль заказа.
  • Service MUST трактовать ответ CDEK "заказ с таким external id уже существует" (возврат существующего entity.uuid либо CDEK-specific duplicate response) как успех, сохранять возвращённый cdek_order_uuid и отвечать OK, а не как ошибку с повторной регистрацией.
  • Не изменять public contract POST /api/v1/delivery/init-payment, price flow и address suggestion flow.
  • Не добавлять новые payment providers, refund flow, recurring payments, ручной retry endpoint или endpoint чтения заявок.
  • Не изменять файлы в spec/.

Acceptance criteria

  • Существует schema TBankPaymentNotification с обязательными полями TerminalKey, OrderId, Success, Status, PaymentId, ErrorCode, Amount, Token и поддержкой дополнительных top-level полей TBank. PaymentId валидируется как положительное целое (TBank присылает long integer) и сохраняется в tbank_payment_id колонке BIGINT.
  • POST /api/v1/delivery/tbank/notifications реализован в существующем controller app/controllers/v1/delivery.py и возвращает PlainTextResponse("OK") при успешной обработке.
  • Controller делегирует обработку ровно в AggregatorService.handle_tbank_payment_notification() и не содержит branching по TBank status.
  • TBankAdapter умеет проверять token payment notification через tbank_payment.auth.password; невалидный token маппится в service/controller error path без repository/CDEK side effects.
  • В app/domain/ есть pure function, которая для Status == "CONFIRMED", Success == true и ErrorCode == "0" возвращает действие регистрации CDEK, а для остальных статусов возвращает действие acknowledge-only.
  • OrderRepository предоставляет primitive для получения заявки по order_uuid, сохранения последнего TBank payment status/payment id и сохранения cdek_order_uuid.
  • Таблица orders содержит минимум поля payment_status, tbank_payment_id (BIGINT), cdek_order_uuid, updated_at в исходной миграции; отдельная миграция не вводится, так как production БД ещё не развёрнута.
  • При валидном CONFIRMED notification service получает order по OrderId, реконструирует InitPaymentRequest из сохранённых данных, вызывает injected CDEK order adapter registration и сохраняет полученный cdek_order_uuid.
  • Если для OrderId уже сохранён cdek_order_uuid, повторный CONFIRMED notification возвращает OK без повторного вызова CDEK.
  • CDEK order mapper проставляет InitPaymentRequest.order_uuid в поле external номера заказа CDEK (number), так что повторный POST с тем же external id не создаёт второй заказ в CDEK.
  • Если CDEK create фактически завершился успешно, но сохранение cdek_order_uuid в DB не прошло, следующий валидный CONFIRMED notification MUST завершиться сохранением того же cdek_order_uuid (полученного по тому же external id) без создания второго заказа в CDEK.
  • Валидные notification со статусами AUTHORIZED, REJECTED, CANCELED, DEADLINE_EXPIRED и неизвестными status values возвращают OK без регистрации CDEK order.
  • Ошибка поиска заявки или ошибка CDEK registration возвращает deterministic non-OK HTTP response и логируется.
  • NotificationURL в runtime/test/example конфигурации, если он присутствует в репозитории, указывает на /api/v1/delivery/tbank/notifications.

Definition of Done

  • Добавлена schema TBankPaymentNotification.
  • Добавлена pure Business Logic для выбора действия по TBank payment notification status.
  • Добавлена token verification logic в TBank adapter.
  • Расширена SQLAlchemy model и обновлена существующая Alembic migration 20260412_028_create_orders_table payment/CDEK status fields (production БД отсутствует, новая миграция не нужна).
  • Расширен OrderRepository primitives для read/update операций, нужных webhook flow.
  • Реализован AggregatorService.handle_tbank_payment_notification() с DI dependencies.
  • Добавлен endpoint POST /api/v1/delivery/tbank/notifications в существующий delivery controller.
  • Wiring использует существующие TBankAdapter, OrderRepository и CDEK provider/adapter без provider HTTP details в Service/Controller.
  • Повторные CONFIRMED notification обрабатываются идемпотентно.
  • CDEK order mapper проставляет order_uuid в поле external номера заказа CDEK.
  • Service корректно обрабатывает ответ CDEK "заказ уже существует" как успех и сохраняет возвращённый cdek_order_uuid.
  • Валидные non-CONFIRMED statuses подтверждаются без CDEK side effects.
  • Обновлены tests для domain, adapter, repository, service, controller и smoke wiring.
  • Все команды из раздела Commands проходят.

Tests

  • Добавить tests/domain/test_payment_notifications.py: CONFIRMED + Success=true + ErrorCode=0 -> create CDEK order action; остальные known/unknown statuses -> acknowledge-only.
  • Обновить или добавить tests/adapters/tbank/test_notifications.py: valid token, invalid token, exclusion of Token, exclusion of nested Data/Receipt, inclusion of extra scalar fields, deterministic SHA-256 comparison.
  • Обновить tests/repositories/order/test_repository.py: получение заявки по order_uuid, сохранение payment_status/tbank_payment_id, сохранение cdek_order_uuid, duplicate/idempotency scenario.
  • Обновить tests/adapters/delivery_providers/cdek/test_order_mapper.py: CDEK order payload содержит InitPaymentRequest.order_uuid в поле external номера заказа (number).
  • Добавить tests/services/test_tbank_notifications.py: valid CONFIRMED вызывает repository lookup, CDEK registration и save cdek_order_uuid; duplicate CONFIRMED не вызывает CDEK; non-CONFIRMED возвращает OK без CDEK; invalid token не обращается к repository; missing order/CDEK failure возвращает service error; сценарий "CDEK create succeeded, save cdek_order_uuid raised" — следующий CONFIRMED notification вызывает CDEK повторно с тем же external id, получает тот же cdek_order_uuid, сохраняет его и возвращает OK (суммарно ровно один реальный заказ в CDEK).
  • Добавить tests/controllers/v1/test_tbank_notifications.py: endpoint возвращает plain text OK для successful service response, 400 для invalid token, 503 для temporary processing failure, controller делегирует ровно один service method.
  • Обновить tests/smoke/test_app_import.py при необходимости для проверки wiring нового endpoint.

Commands

  • poetry run pytest tests/domain/test_payment_notifications.py -q
  • poetry run pytest tests/adapters/tbank/test_notifications.py -q
  • poetry run pytest tests/repositories/order/test_repository.py -q
  • poetry run pytest tests/services/test_tbank_notifications.py -q
  • poetry run pytest tests/controllers/v1/test_tbank_notifications.py -q
  • poetry run pytest tests/smoke/test_app_import.py -q
  • poetry run pytest -q
  • python3 spec/gen_spec_index.py --check