Files
g2s-aggregator/spec/overview.md
T
2026-03-29 02:05:21 +03:00

254 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# spec/overview.md
Контекст, специфичный для проекта агрегатора служб доставки.
Глобальные правила определены в AGENTS.md.
---
## Проект
**Название:** Агрегатор служб доставки (бэкенд)
**Язык:** Python 3.14
**Назначение:** Агрегировать расчёты стоимости доставки от нескольких служб и отдавать единый ответ фронтенду через унифицированный API.
---
## Продуктовые требования
- Принимать запрос на расчёт стоимости доставки (идентификаторы городов отправления/назначения, вес, габариты)
- Поддерживать необязательный параметр `parcel_type` в запросе расчёта стоимости доставки для фильтрации тарифов по типу отправления
- Предоставлять отдельный endpoint подсказок адреса, чтобы frontend мог получить точное значение для `from_location.address` и `to_location.address` перед созданием заказа
- Принимать запрос на создание заказа CDEK по контракту из `http-client.http` для сценария "доставка, до двери"
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
- Для стран, сопоставленных с provider id `dadata`, использовать `dadata.ru`; конфигурация и wiring должны допускать отдельный address suggestion provider для европейских стран
- Опрашивать всех зарегистрированных провайдеров параллельно
- Возвращать унифицированный список тарифов, отсортированных по цене
- Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
- Кешировать ответы провайдеров для исключения повторных внешних запросов. Время кэширования вынести в конфиг
- Применять к цене, полученной от провайдера, конфигурируемый мультипликатор и округлять итоговую цену до целого значения
- Для расчёта стоимости использовать локальный справочник `cities_map` с provider-specific данными города; для CDEK брать `cdek.code` без обращения к API подсказок городов
---
## Провайдеры
### Фаза 1
- **CDEK** — https://apidoc.cdek.ru/
- Аутентификация: OAuth2 (client credentials)
- Операции: расчёт тарифа, регистрация заказа
- Cache TTL: 15 минут
### Фаза 2+
- Дополнительные провайдеры (Boxberry, DHL и др.) — подключаются через интерфейс `DeliveryProvider` без изменений в логике агрегации
---
## Компоненты
### Controller (`app/controllers/v1/delivery.py`)
- `POST /api/v1/delivery/price` — принимает `DeliveryCalculationRequest`, возвращает `list[DeliveryPrice]`
- `POST /api/v1/delivery/suggest-address` — принимает `AddressSuggestRequest`, возвращает `list[AddressSuggestion]`
- `POST /api/v1/delivery/order` — принимает `OrderCreateRequest`, возвращает `OrderCreateResponse`
- Парсинг и валидация входных данных через Pydantic
- Маппинг исключений сервиса в HTTP-ответы
- Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()`, `AggregatorService.suggest_addresses()` или `AggregatorService.create_order()`
### Service (`app/services/aggregator.py`)
- `AggregatorService.get_all_prices(request: DeliveryCalculationRequest) -> list[DeliveryPrice]`
- Распределяет запросы по всем зарегистрированным провайдерам через `asyncio.gather(..., return_exceptions=True)`
- Принимает от каждого провайдера список тарифов и объединяет их в единый список
- Фильтрует упавшие результаты
- Сортирует тарифы по цене
- `AggregatorService.suggest_addresses(request: AddressSuggestRequest) -> list[AddressSuggestion]`
- `AggregatorService.suggest_addresses()` выбирает address suggestion provider по `country_code` через injected config mapping и оркестрирует ровно один adapter call
- `AggregatorService.create_order(request: OrderCreateRequest) -> OrderCreateResponse`
- `AggregatorService.create_order()` оркестрирует регистрацию заказа в CDEK через injected adapter dependency
- Service не содержит бизнес-логики и provider HTTP-деталей
### Business Logic (`app/domain/`)
- Правила сортировки и фильтрации тарифов
- Правила фильтрации тарифов по `parcel_type`
- Логика сравнения цен
- Правила применения конфигурируемого мультипликатора к ценам провайдеров
- Округление цены после применения мультипликатора до целого значения по детерминированному правилу
- Нормализация входных данных (например, идентификаторов городов и правил округления веса)
- Чистые функции, без IO, без зависимостей от фреймворков
### Repository (`app/repositories/cache/`)
- `PriceCache` — хранилище ключ/значение на базе Redis
- Операции: `get(key)`, `set(key, value, ttl)`, `invalidate(key)`
- Без бизнес-решений; формирование ключа — ответственность Adapter
### Adapter (`app/adapters/delivery_providers`)
- `base.py` — абстрактный интерфейс `DeliveryProvider`:
```python
class DeliveryProvider(ABC):
name: str
async def get_prices(self, request: DeliveryCalculationRequest) -> list[DeliveryPrice]: ...
```
- `cdek/client.py` — HTTP-клиент (httpx AsyncClient), аутентификация, ретраи, таймаут (10с)
- `cdek/auth.py` — управление OAuth2-токеном
- `cdek/mapper.py` — ответ CDEK → `list[DeliveryPrice]`
- `cdek/order_mapper.py` — request/response mapping для регистрации заказа CDEK
- Каждый адаптер владеет своей конфигурацией; наружу экспонирует только service-facing methods, необходимые соответствующему use-case
- Для расчёта тарифа CDEK adapter принимает city identifiers из `DeliveryCalculationRequest`, находит запись в `cities_map`, берёт `cdek.code` и передаёт его в CDEK API
Для сценария создания заказа CDEK adapter принимает валидированную order model, отправляет контракт `Регистрация заказа (тип "доставка", до двери)` из `http-client.http` и возвращает внутреннюю response model без утечки HTTP-деталей в Service.
### Adapter (`app/adapters/address_suggestions`)
- `base.py` — абстрактный интерфейс `AddressSuggestionProvider`:
```python
class AddressSuggestionProvider(ABC):
name: str
async def suggest(self, request: AddressSuggestRequest) -> list[AddressSuggestion]: ...
```
- `dadata/client.py` — HTTP-клиент `dadata.ru` для address suggestions
- provider-specific модули address suggestion adapters инкапсулируют внешние API-контракты, auth, serialization и error handling
- В конфиге adapter layer хранится маппинг `country_code -> provider_id` для выбора address suggestion provider
- Address suggestion adapters возвращают только унифицированные internal models без утечки provider-specific payload в Service
---
## Reference Data
- `cities_map` содержит city identifier и provider-specific данные города для price flow.
- Справочник может быть частично заполнен; отсутствие provider-specific данных для города должно обрабатываться детерминированно.
- Структура справочника должна допускать появление дополнительных провайдеров без изменения публичного API расчёта.
---
## Модели данных
### Входная: `DeliveryCalculationRequest`
```
entity: Enum(individual, legal)
from_city: int
to_city: int
weight_kg: float
length_cm: float
width_cm: float
height_cm: float
parcel_type: Literal[doc, parcel] | None
```
### Выходная: `DeliveryPrice`
```
provider: str
service_name: str
price: Decimal
currency: str
delivery_days_min: int
delivery_days_max: int
```
### Входная: `AddressSuggestRequest`
```
country_code: str
city: str
query: str
limit: int | None
```
### Выходная: `AddressSuggestion`
```
address: str
street: str | None
house: str | None
flat: str | None
postal_code: str | None
```
### Входная: `OrderCreateRequest`
```
type: Literal[2]
tariff_code: Literal[535]
comment: str | None
sender:
company: str | None
name: str
email: str
phones: list[{number: str}]
recipient:
name: str
email: str
phones: list[{number: str}]
from_location:
address: str
city: str
country_code: str
to_location:
address: str
city: str
country_code: str
services: list[{code: str, parameter: str}]
packages: list[{number: str, weight: int, length: int, width: int, height: int, comment: str | None}]
```
`from_location.address` и `to_location.address` должны содержать точные значения адреса, выбранные клиентом; order flow не выполняет address suggestion lookup.
### Выходная: `OrderCreateResponse`
```
provider: str
order_uuid: str
```
---
## Технологический стек
| Задача | Инструмент |
|---|---|
| Web-фреймворк | FastAPI |
| HTTP-клиент | httpx (async) |
| Валидация | Pydantic v2 |
| Кеш | Redis |
| Конфигурация | pydantic-settings |
| Сервер | Uvicorn |
---
## Структура проекта
```
app/
├── controllers/
│ ├── http_client.py
│ └── v1/
│ └── delivery.py # Controller
├── services/
│ └── aggregator.py # Service
├── domain/
│ └── price.py # Business Logic
├── adapters/
│ ├── address_suggestions/
│ │ ├── base.py # Интерфейс Adapter
│ │ └── dadata/
│ │ └── client.py
│ └── delivery_providers/
│ ├── base.py # Интерфейс Adapter
│ └── cdek/
│ ├── client.py
│ ├── auth.py
│ ├── mapper.py
│ └── order_mapper.py
├── repositories/
│ └── cache/
│ └── redis_cache.py # Repository
├── schemas/
│ ├── address.py
│ ├── request.py
│ ├── response.py
│ └── order.py
└── config.py
```
---
## Инфраструктура
```yaml
# docker-compose сервисы
app # FastAPI-приложение
redis # Кеш тарифов
```