# spec/overview.md Контекст, специфичный для проекта агрегатора служб доставки. Глобальные правила определены в AGENTS.md. --- ## Проект **Название:** Агрегатор служб доставки (бэкенд) **Язык:** Python 3.14 **Назначение:** Агрегировать расчёты стоимости доставки от нескольких служб и отдавать единый ответ фронтенду через унифицированный API. --- ## Продуктовые требования - Принимать запрос на расчёт стоимости доставки (идентификаторы городов отправления/назначения, вес, габариты) - Поддерживать необязательный параметр `parcel_type` в запросе расчёта стоимости доставки для фильтрации тарифов по типу отправления - Принимать запрос на создание заказа CDEK по контракту из `http-client.http` для сценария "доставка, до двери" - Опрашивать всех зарегистрированных провайдеров параллельно - Возвращать унифицированный список тарифов, отсортированных по цене - Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком - Кешировать ответы провайдеров для исключения повторных внешних запросов. Время кэширования вынести в конфиг - Применять к цене, полученной от провайдера, конфигурируемый мультипликатор и округлять итоговую цену до целого значения - Для расчёта стоимости использовать локальный справочник `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/order` — принимает `OrderCreateRequest`, возвращает `OrderCreateResponse` - Парсинг и валидация входных данных через Pydantic - Маппинг исключений сервиса в HTTP-ответы - Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()` или `AggregatorService.create_order()` ### Service (`app/services/aggregator.py`) - `AggregatorService.get_all_prices(request: DeliveryCalculationRequest) -> list[DeliveryPrice]` - Распределяет запросы по всем зарегистрированным провайдерам через `asyncio.gather(..., return_exceptions=True)` - Принимает от каждого провайдера список тарифов и объединяет их в единый список - Фильтрует упавшие результаты - Сортирует тарифы по цене - `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. --- ## 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 ``` ### Входная: `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}] ``` ### Выходная: `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/ │ └── delivery_providers/ │ ├── base.py # Интерфейс Adapter │ └── cdek/ │ ├── client.py │ ├── auth.py │ ├── mapper.py │ └── order_mapper.py ├── repositories/ │ └── cache/ │ └── redis_cache.py # Repository ├── schemas/ │ ├── request.py │ ├── response.py │ └── order.py └── config.py ``` --- ## Инфраструктура ```yaml # docker-compose сервисы app # FastAPI-приложение redis # Кеш тарифов ```