12 KiB
12 KiB
id, title, status, created
| id | title | status | created |
|---|---|---|---|
| 030 | Add TBank payment notification webhook and CDEK order creation | DONE | 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 bodyOK. - Валидные уведомления со статусами, отличными от
CONFIRMED, MUST возвращатьOKбез чтения CDEK и без создания заказа. - Повторное валидное уведомление
CONFIRMEDдля заявки с уже сохранённымcdek_order_uuidMUST возвращать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/order, 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реализован в существующем controllerapp/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 БД ещё не развёрнута. - При валидном
CONFIRMEDnotification service получает order поOrderId, реконструируетInitPaymentRequestиз сохранённых данных, вызывает injected CDEK order adapter registration и сохраняет полученныйcdek_order_uuid. - Если для
OrderIdуже сохранёнcdek_order_uuid, повторныйCONFIRMEDnotification возвращаетOKбез повторного вызова CDEK. - CDEK order mapper проставляет
InitPaymentRequest.order_uuidв поле external номера заказа CDEK (number), так что повторный POST с тем же external id не создаёт второй заказ в CDEK. - Если CDEK create фактически завершился успешно, но сохранение
cdek_order_uuidв DB не прошло, следующий валидныйCONFIRMEDnotification 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_tablepayment/CDEK status fields (production БД отсутствует, новая миграция не нужна). - Расширен
OrderRepositoryprimitives для 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. - Повторные
CONFIRMEDnotification обрабатываются идемпотентно. - CDEK order mapper проставляет
order_uuidв поле external номера заказа CDEK. - Service корректно обрабатывает ответ CDEK "заказ уже существует" как успех и сохраняет возвращённый
cdek_order_uuid. - Валидные non-
CONFIRMEDstatuses подтверждаются без 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 ofToken, exclusion of nestedData/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: validCONFIRMEDвызывает repository lookup, CDEK registration и savecdek_order_uuid; duplicateCONFIRMEDне вызывает CDEK; non-CONFIRMEDвозвращаетOKбез CDEK; invalid token не обращается к repository; missing order/CDEK failure возвращает service error; сценарий "CDEK create succeeded, savecdek_order_uuidraised" — следующийCONFIRMEDnotification вызывает CDEK повторно с тем же external id, получает тот жеcdek_order_uuid, сохраняет его и возвращаетOK(суммарно ровно один реальный заказ в CDEK). - Добавить
tests/controllers/v1/test_tbank_notifications.py: endpoint возвращает plain textOKдля 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 -qpoetry run pytest tests/adapters/tbank/test_notifications.py -qpoetry run pytest tests/repositories/order/test_repository.py -qpoetry run pytest tests/services/test_tbank_notifications.py -qpoetry run pytest tests/controllers/v1/test_tbank_notifications.py -qpoetry run pytest tests/smoke/test_app_import.py -qpoetry run pytest -qpython3 spec/gen_spec_index.py --check