Добавлена обработка уведомлений через ручку
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
---
|
||||
id: 030
|
||||
title: Add TBank payment notification webhook and CDEK order creation
|
||||
status: TODO
|
||||
created: 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`
|
||||
Reference in New Issue
Block a user