Files
g2s-aggregator/spec/overview.md
T
Раис Юсупалиев 15a17be4ed 016 add creating order to adapter
2026-03-14 03:55:23 +03:00

8.0 KiB

spec/overview.md

Контекст, специфичный для проекта агрегатора служб доставки. Глобальные правила определены в AGENTS.md.


Проект

Название: Агрегатор служб доставки (бэкенд) Язык: Python 3.14 Назначение: Агрегировать расчёты стоимости доставки от нескольких служб и отдавать единый ответ фронтенду через унифицированный API.


Продуктовые требования

  • Принимать запрос на расчёт стоимости доставки (откуда, куда, вес, габариты)
  • Принимать запрос на создание заказа CDEK по контракту из http-client.http для сценария "доставка, до двери"
  • Опрашивать всех зарегистрированных провайдеров параллельно
  • Возвращать унифицированный список тарифов, отсортированных по цене
  • Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
  • Кешировать ответы провайдеров для исключения повторных внешних запросов. Время кэширования вынести в конфиг
  • Применять к цене, полученной от провайдера, конфигурируемый мультипликатор и округлять итоговую цену до целого значения

Провайдеры

Фаза 1

  • CDEKhttps://apidoc.cdek.ru/
    • Аутентификация: OAuth2 (client credentials)
    • Операции: расчёт тарифа, регистрация заказа
    • Cache TTL: 15 минут

Фаза 2+

  • Дополнительные провайдеры (Boxberry, DHL и др.) — подключаются через интерфейс DeliveryProvider без изменений в логике агрегации

Компоненты

Controller (app/controllers/v1/delivery.py)

  • POST /api/v1/delivery/price — принимает DeliveryRequest, возвращает 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: DeliveryRequest) -> 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/)

  • Правила сортировки и фильтрации тарифов
  • Логика сравнения цен
  • Правила применения конфигурируемого мультипликатора к ценам провайдеров
  • Округление цены после применения мультипликатора до целого значения по детерминированному правилу
  • Нормализация входных данных (например, правила округления веса)
  • Чистые функции, без IO, без зависимостей от фреймворков

Repository (app/repositories/cache/)

  • PriceCache — хранилище ключ/значение на базе Redis
  • Операции: get(key), set(key, value, ttl), invalidate(key)
  • Без бизнес-решений; формирование ключа — ответственность Adapter

Adapter (app/adapters/delivery_providers)

  • base.py — абстрактный интерфейс DeliveryProvider:
    class DeliveryProvider(ABC):
        name: str
        async def get_price(self, request: DeliveryRequest) -> DeliveryPrice: ...
    
  • cdek/client.py — HTTP-клиент (httpx AsyncClient), аутентификация, ретраи, таймаут (10с)
  • cdek/auth.py — управление OAuth2-токеном
  • cdek/mapper.py — ответ CDEK → DeliveryPrice
  • cdek/order_mapper.py — request/response mapping для регистрации заказа CDEK
  • Каждый адаптер владеет своей конфигурацией; наружу экспонирует только service-facing methods, необходимые соответствующему use-case

Для сценария создания заказа CDEK adapter принимает валидированную order model, отправляет контракт Регистрация заказа (тип "доставка", до двери) из http-client.http и возвращает внутреннюю response model без утечки HTTP-деталей в Service.


Модели данных

Входная: DeliveryRequest

entity: Enum(individual, legal)
from_city: str
to_city: str
weight_kg: float
length_cm: float
width_cm: float
height_cm: float

Выходная: 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[136]
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

Инфраструктура

# docker-compose сервисы
app        # FastAPI-приложение
redis      # Кеш тарифов