Добавлена обработка уведомлений через ручку

This commit is contained in:
Раис Юсупалиев
2026-04-18 21:27:15 +03:00
parent 78e9ad4fa9
commit 6b66af5eb3
38 changed files with 1519 additions and 32 deletions
+37 -2
View File
@@ -21,6 +21,9 @@
- Принимать запрос на инициализацию оплаты доставки через TBank и возвращать ссылку на оплату без регистрации заказа в CDEK
- Принимать сумму оплаты в поле `price` в копейках
- После успешного получения ссылки на оплату от TBank сохранять все данные заявки вместе со ссылкой на оплату в PostgreSQL; ошибка сохранения не блокирует возврат ссылки клиенту
- Принимать HTTP-уведомления TBank о статусах платежа на отдельный webhook endpoint
- При получении валидного уведомления TBank со статусом `CONFIRMED` находить сохранённую заявку в PostgreSQL и регистрировать заказ в CDEK
- Остальные валидные статусы платежа TBank подтверждать без регистрации заказа в CDEK
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
- Для `RU`, `BY` и `KZ`, сопоставленных с provider id `dadata`, использовать `dadata.ru`
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ`, сопоставленных с provider id `yandex_geosuggest`, использовать Yandex Geosuggest
@@ -44,7 +47,7 @@
- Cache TTL: 15 минут
- **TBank** — https://securepay.tinkoff.ru/v2/Init
- Аутентификация: `TerminalKey` и token на основе password
- Операции: инициализация платежа и получение payment URL
- Операции: инициализация платежа, получение payment URL, проверка подписи HTTP-уведомлений
### Фаза 2+
- Дополнительные провайдеры (Boxberry, DHL и др.) — подключаются через интерфейс `DeliveryProvider` без изменений в логике агрегации
@@ -57,9 +60,10 @@
- `POST /api/v1/delivery/price` — принимает `DeliveryCalculationRequest`, возвращает `list[DeliveryPrice]`
- `POST /api/v1/delivery/suggest-address` — принимает `AddressSuggestRequest`, возвращает `list[AddressSuggestion]`
- `POST /api/v1/delivery/init-payment` — принимает `InitPaymentRequest`, возвращает `InitPaymentResponse`
- `POST /api/v1/delivery/tbank/notifications` — принимает `TBankPaymentNotification`, возвращает plain text `OK` при успешной обработке
- Парсинг и валидация входных данных через Pydantic
- Маппинг исключений сервиса в HTTP-ответы
- Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()`, `AggregatorService.suggest_addresses()` или `AggregatorService.init_payment()`
- Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()`, `AggregatorService.suggest_addresses()`, `AggregatorService.init_payment()` или `AggregatorService.handle_tbank_payment_notification()`
### Service (`app/services/aggregator.py`)
- `AggregatorService.get_all_prices(request: DeliveryCalculationRequest) -> list[DeliveryPrice]`
@@ -71,6 +75,10 @@
- `AggregatorService.suggest_addresses()` выбирает address suggestion provider по `country_code` через injected config mapping и оркестрирует ровно один adapter call
- `AggregatorService.init_payment(request: InitPaymentRequest) -> InitPaymentResponse`
- `AggregatorService.init_payment()` оркестрирует инициализацию платежа через injected TBank adapter dependency, затем сохраняет данные заявки вместе с `payment_url` через injected order repository; ошибка сохранения логируется, но не блокирует возврат `payment_url`
- `AggregatorService.handle_tbank_payment_notification(notification: TBankPaymentNotification) -> str`
- `AggregatorService.handle_tbank_payment_notification()` оркестрирует проверку подписи уведомления через injected TBank adapter, определение действия по статусу через Business Logic, чтение сохранённой заявки через injected order repository и регистрацию заказа через injected CDEK adapter при статусе `CONFIRMED`
- Повторное валидное уведомление `CONFIRMED` для заявки с уже сохранённым `cdek_order_uuid` не должно повторно создавать заказ в CDEK
- Валидные уведомления TBank со статусами, отличными от `CONFIRMED`, подтверждаются ответом `OK` без обращения к CDEK
- Service не содержит бизнес-логики и provider HTTP-деталей
### Business Logic (`app/domain/`)
@@ -79,6 +87,7 @@
- Логика сравнения цен
- Правила применения конфигурируемого мультипликатора к ценам провайдеров
- Округление цены после применения мультипликатора до целого значения по детерминированному правилу
- Правило выбора действия для TBank payment notification по статусу платежа
- Нормализация входных данных (например, идентификаторов городов и правил округления веса)
- Чистые функции, без IO, без зависимостей от фреймворков
@@ -90,6 +99,9 @@
### Repository (`app/repositories/order/`)
- `OrderRepository` — сохранение данных заявки в PostgreSQL
- Операции: `create_order(session, order_data)` — сохраняет запись заявки с данными `InitPaymentRequest` и `payment_url`
- Операции: `get_order_by_order_uuid(session, order_uuid)` — получает сохранённую заявку для payment notification flow
- Операции: `mark_payment_status(session, order_uuid, status, payment_id)` — сохраняет последний статус платежа TBank
- Операции: `mark_cdek_order_registered(session, order_uuid, cdek_order_uuid)` — сохраняет UUID заказа CDEK после успешной регистрации
- SQLAlchemy ORM model таблицы `orders` в `models.py`
- Без бизнес-решений; только CRUD-примитивы
@@ -113,6 +125,7 @@
- `base.py` — исключения TBank payment adapter
- `client.py` — `TBankAdapter`
- `TBankAdapter.create_payment_link(order_uuid: str, amount_kopecks: int) -> str` инициализирует платёж TBank и возвращает payment URL
- `TBankAdapter.verify_payment_notification(notification: TBankPaymentNotification) -> None` проверяет token HTTP-уведомления TBank по top-level scalar fields, исключая `Token` и вложенные объекты
- TBank adapter инкапсулирует HTTP-взаимодействие с TBank Init API, auth token, retries, timeout, serialization и error handling
- TBank adapter владеет собственной секцией конфигурации `tbank_payment` с полями `init_url`, `auth.terminal_key`, `auth.password`, `timeout_seconds`, `retry_attempts`, `retry_backoff_seconds`
@@ -226,6 +239,28 @@ packages: list[{number: str, weight: int, length: int, width: int, height: int,
payment_url: str
```
### Входная: `TBankPaymentNotification`
```
TerminalKey: str
OrderId: str
Success: bool
Status: str
PaymentId: int
ErrorCode: str
Amount: int
Token: str
```
`PaymentId` приходит из TBank как JSON-число (long integer) и валидируется как положительное целое.
Модель уведомления должна допускать дополнительные top-level поля TBank. Проверка token использует все top-level scalar fields кроме `Token`, добавляет `Password` из `tbank_payment.auth.password`, сортирует ключи по алфавиту, конкатенирует строковые представления значений (булевы — как lowercase `true`/`false`) и сравнивает SHA-256 hash с `Token`. Вложенные объекты, включая `Data` и `Receipt`, не участвуют в проверке token.
### Выходная: TBank notification response
```
OK
```
Успешно обработанное HTTP-уведомление TBank возвращает `HTTP 200` и plain text body `OK`.
---
## Технологический стек