Добавлено сохранение заказов в postgres
This commit is contained in:
+3
-2
@@ -34,9 +34,10 @@
|
||||
| 025 | DONE | 2026-04-03 | Remove company from Create Delivery Order parties | `spec/tasks/025_remove_company_from_create_delivery_order.md` |
|
||||
| 026 | DONE | 2026-04-05 | Align CDEK order contract with single phone and kilogram package weight | `spec/tasks/026_align_cdek_order_contract_single_phone_and_weight_units.md` |
|
||||
| 027 | DONE | 2026-04-11 | Add TBank payment adapter, init_payment endpoint and rename order flow | `spec/tasks/027_add_tbank_payment_adapter_and_order_payment_link.md` |
|
||||
| 028 | DONE | 2026-04-12 | Add PostgreSQL adapter, order repository and persist order after payment link creation | `spec/tasks/028_add_postgresql_order_persistence.md` |
|
||||
|
||||
## Summary
|
||||
|
||||
- Total: **28**
|
||||
- Total: **29**
|
||||
- TODO: **0**
|
||||
- DONE: **28**
|
||||
- DONE: **29**
|
||||
|
||||
+27
-4
@@ -20,6 +20,7 @@
|
||||
- Предоставлять отдельный endpoint подсказок адреса, чтобы frontend мог получить точное значение для `from_location.address` и `to_location.address` перед инициализацией оплаты доставки
|
||||
- Принимать запрос на инициализацию оплаты доставки через TBank и возвращать ссылку на оплату без регистрации заказа в CDEK
|
||||
- Принимать сумму оплаты в поле `price` в копейках
|
||||
- После успешного получения ссылки на оплату от TBank сохранять все данные заявки вместе со ссылкой на оплату в PostgreSQL; ошибка сохранения не блокирует возврат ссылки клиенту
|
||||
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
|
||||
- Для `RU`, `BY` и `KZ`, сопоставленных с provider id `dadata`, использовать `dadata.ru`
|
||||
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ`, сопоставленных с provider id `yandex_geosuggest`, использовать Yandex Geosuggest
|
||||
@@ -69,7 +70,7 @@
|
||||
- `AggregatorService.suggest_addresses(request: AddressSuggestRequest) -> list[AddressSuggestion]`
|
||||
- `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
|
||||
- `AggregatorService.init_payment()` оркестрирует инициализацию платежа через injected TBank adapter dependency, затем сохраняет данные заявки вместе с `payment_url` через injected order repository; ошибка сохранения логируется, но не блокирует возврат `payment_url`
|
||||
- Service не содержит бизнес-логики и provider HTTP-деталей
|
||||
|
||||
### Business Logic (`app/domain/`)
|
||||
@@ -86,6 +87,12 @@
|
||||
- Операции: `get(key)`, `set(key, value, ttl)`, `invalidate(key)`
|
||||
- Без бизнес-решений; формирование ключа — ответственность Adapter
|
||||
|
||||
### Repository (`app/repositories/order/`)
|
||||
- `OrderRepository` — сохранение данных заявки в PostgreSQL
|
||||
- Операции: `create_order(session, order_data)` — сохраняет запись заявки с данными `InitPaymentRequest` и `payment_url`
|
||||
- SQLAlchemy ORM model таблицы `orders` в `models.py`
|
||||
- Без бизнес-решений; только CRUD-примитивы
|
||||
|
||||
### Adapter (`app/adapters/delivery_providers`)
|
||||
- `base.py` — абстрактный интерфейс `DeliveryProvider`:
|
||||
```python
|
||||
@@ -109,6 +116,12 @@
|
||||
- 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`
|
||||
|
||||
### Adapter (`app/adapters/postgres`)
|
||||
- Управление подключением к PostgreSQL: создание `AsyncEngine` и `async_sessionmaker` из конфигурации
|
||||
- Инкапсулирует инфраструктурные детали SQLAlchemy async engine
|
||||
- Владеет собственной секцией конфигурации `postgres` с обязательным полем `dsn`
|
||||
- Без SQL-запросов, без бизнес-логики
|
||||
|
||||
### Adapter (`app/adapters/address_suggestions`)
|
||||
- `base.py` — абстрактный интерфейс `AddressSuggestionProvider`:
|
||||
```python
|
||||
@@ -223,6 +236,8 @@ payment_url: str
|
||||
| HTTP-клиент | httpx (async) |
|
||||
| Валидация | Pydantic v2 |
|
||||
| Кеш | Redis |
|
||||
| База данных | PostgreSQL (asyncpg + SQLAlchemy 2.0 async) |
|
||||
| Миграции | Alembic (async) |
|
||||
| Конфигурация | pydantic-settings |
|
||||
| Сервер | Uvicorn |
|
||||
|
||||
@@ -259,15 +274,22 @@ app/
|
||||
│ └── tbank/
|
||||
│ ├── base.py
|
||||
│ └── client.py
|
||||
│ └── postgres/
|
||||
│ └── engine.py # AsyncEngine & session factory
|
||||
├── repositories/
|
||||
│ └── cache/
|
||||
│ └── redis_cache.py # Repository
|
||||
│ ├── cache/
|
||||
│ │ └── redis_cache.py # Repository (Redis)
|
||||
│ └── order/
|
||||
│ ├── models.py # SQLAlchemy ORM model
|
||||
│ └── repository.py # Repository (PostgreSQL)
|
||||
├── schemas/
|
||||
│ ├── address.py
|
||||
│ ├── request.py
|
||||
│ ├── response.py
|
||||
│ └── payment.py
|
||||
└── config.py
|
||||
├── config.py
|
||||
alembic/ # Alembic migrations
|
||||
alembic.ini
|
||||
```
|
||||
|
||||
---
|
||||
@@ -278,4 +300,5 @@ app/
|
||||
# docker-compose сервисы
|
||||
app # FastAPI-приложение
|
||||
redis # Кеш тарифов
|
||||
postgres # База данных заявок
|
||||
```
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: 028
|
||||
title: Add PostgreSQL adapter, order repository and persist order after payment link creation
|
||||
status: DONE
|
||||
created: 2026-04-12
|
||||
---
|
||||
|
||||
## Context
|
||||
После создания ссылки на оплату через TBank adapter данные заявки нигде не сохраняются. Необходимо добавить PostgreSQL-адаптер для управления подключением к базе данных, репозиторий для сохранения данных заявки и интегрировать сохранение в существующий flow `AggregatorService.init_payment()`. Зависимости `sqlalchemy` (2.0.49) и `asyncpg` (0.31.0) уже присутствуют в `pyproject.toml`. Alembic (1.18.4) также доступен для миграций.
|
||||
|
||||
## Goal
|
||||
1. Добавить PostgreSQL-адаптер (`app/adapters/postgres/`) для управления async-сессиями SQLAlchemy (`AsyncEngine`, `async_sessionmaker`).
|
||||
2. Добавить репозиторий заявок (`app/repositories/order/`) с методом `create_order()` для сохранения данных заявки вместе со ссылкой на оплату в PostgreSQL.
|
||||
3. Определить SQLAlchemy model для таблицы заявок в `app/repositories/order/models.py`.
|
||||
4. Создать Alembic-миграцию для создания таблицы заявок.
|
||||
5. Расширить `AggregatorService.init_payment()`: после успешного получения `payment_url` от TBank adapter сохранять данные `InitPaymentRequest` вместе с `payment_url` через order repository.
|
||||
6. Добавить секцию конфигурации PostgreSQL (`PostgresConfig`) в `app/config.py` и пример в `config.yaml`.
|
||||
7. Добавить сервис PostgreSQL в `docker-compose.yml`.
|
||||
|
||||
## Constraints
|
||||
- PostgreSQL adapter (`app/adapters/postgres/`) MUST содержать только управление подключением (engine, session factory). Без бизнес-логики, без SQL-запросов.
|
||||
- Repository (`app/repositories/order/`) MUST содержать только операции с базой данных. Без бизнес-решений, без workflow-логики.
|
||||
- Service MUST оркестрировать вызовы TBank adapter и order repository. Если сохранение в БД завершается ошибкой после успешного получения `payment_url`, Service MUST всё равно вернуть `payment_url` клиенту (сохранение не должно блокировать ответ); ошибку сохранения логировать.
|
||||
- SQLAlchemy model MUST использовать `sqlalchemy.orm.DeclarativeBase` (SQLAlchemy 2.0 style).
|
||||
- Для миграций использовать Alembic с async-конфигурацией (`asyncpg`).
|
||||
- Конфигурация PostgreSQL MUST быть в отдельной секции `postgres` в `config.yaml` с обязательным полем `dsn`; `config.yaml` уже в `.gitignore`.
|
||||
- `_RequiredYamlSections` в `app/config.py` MUST быть обновлён для включения секции `postgres`.
|
||||
- Order repository передаётся в `AggregatorService` через dependency injection (новый параметр конструктора).
|
||||
- PostgreSQL adapter создаётся в wiring (`_build_aggregator_service`) в controller и передаёт session factory в order repository.
|
||||
- Таблица заявок MUST содержать как минимум: `id` (UUID, PK), `order_uuid` (str, unique), `payment_url` (str), `price` (int, копейки), `tariff_code` (int), `sender` (JSONB), `recipient` (JSONB), `from_location` (JSONB), `to_location` (JSONB), `packages` (JSONB), `services` (JSONB, nullable), `comment` (str, nullable), `created_at` (timestamp with timezone, server default).
|
||||
- Scope НЕ включает: чтение/обновление/удаление заявок, API-endpoint для списка заявок, webhook-обработку платёжных уведомлений, изменения price flow, address suggestion flow.
|
||||
- НЕ изменять существующие тесты TBank adapter, не изменять поведение price и address suggestion endpoints.
|
||||
|
||||
## Acceptance criteria
|
||||
- В `app/adapters/postgres/` существует модуль с функцией создания `AsyncEngine` и `async_sessionmaker` из конфигурации.
|
||||
- В `app/repositories/order/` существует `OrderRepository` с async-методом `create_order(session, order_data)`, сохраняющим запись заявки.
|
||||
- В `app/repositories/order/models.py` определена SQLAlchemy ORM model таблицы `orders` со всеми обязательными полями.
|
||||
- Alembic инициализирован с async-конфигурацией; существует миграция для создания таблицы `orders`.
|
||||
- `AggregatorService.__init__()` принимает опциональный `order_repository` через DI.
|
||||
- `AggregatorService.init_payment()` после успешного получения `payment_url` вызывает `order_repository.create_order()` с данными из `InitPaymentRequest` и `payment_url`.
|
||||
- Если `order_repository.create_order()` выбрасывает исключение, `init_payment()` логирует ошибку и возвращает `InitPaymentResponse(payment_url=...)` без ошибки клиенту.
|
||||
- В `app/config.py` добавлена `PostgresConfig` с полем `dsn: str`.
|
||||
- Секция `postgres` присутствует в `_RequiredYamlSections`.
|
||||
- В `docker-compose.yml` добавлен сервис `postgres` и `app` зависит от него.
|
||||
- Wiring в `_build_aggregator_service` создаёт PostgreSQL engine, session factory, `OrderRepository` и передаёт его в `AggregatorService`.
|
||||
- Запросы к эндпоинту `POST /api/v1/delivery/init-payment` продолжают возвращать `InitPaymentResponse` с `payment_url`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Создан модуль `app/adapters/postgres/` с engine/session factory.
|
||||
- [ ] Создан `app/repositories/order/repository.py` с `OrderRepository.create_order()`.
|
||||
- [ ] Создан `app/repositories/order/models.py` с ORM model таблицы `orders`.
|
||||
- [ ] Alembic инициализирован (`alembic.ini`, `alembic/`), создана миграция для таблицы `orders`.
|
||||
- [ ] Добавлена `PostgresConfig` в `app/config.py`; `_RequiredYamlSections` обновлён.
|
||||
- [ ] `AggregatorService` принимает `order_repository` через DI и использует его в `init_payment()`.
|
||||
- [ ] Ошибки сохранения заявки не блокируют возврат `payment_url` клиенту.
|
||||
- [ ] Обновлён wiring в `app/controllers/v1/delivery.py`.
|
||||
- [ ] Добавлен сервис `postgres` в `docker-compose.yml`.
|
||||
- [ ] `config.yaml` пример содержит секцию `postgres`.
|
||||
- [ ] `config.test.yaml` содержит секцию `postgres` (может использовать sqlite или тестовый DSN).
|
||||
- [ ] Все существующие тесты продолжают проходить.
|
||||
- [ ] Добавлены новые тесты.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/repositories/order/test_repository.py`: проверка `create_order()` с in-memory SQLite async engine (SQLAlchemy async); проверка, что все обязательные поля сохраняются; проверка обработки дублирования `order_uuid` (unique constraint).
|
||||
- Обновить `tests/services/test_init_payment.py`: добавить test case, где `order_repository.create_order()` вызывается после успешного создания payment link; добавить test case, где `order_repository.create_order()` выбрасывает исключение, а `init_payment()` всё равно возвращает `payment_url`.
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки наличия секции `postgres` в yaml.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки wiring order repository.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/repositories/order/test_repository.py -q`
|
||||
- `poetry run pytest tests/services/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.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