--- 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/order` продолжают возвращать `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`