Добавлены задачи на сервис подсказок
This commit is contained in:
@@ -3,3 +3,4 @@
|
|||||||
/config.yaml
|
/config.yaml
|
||||||
__pycache__
|
__pycache__
|
||||||
http-client.private.env.json
|
http-client.private.env.json
|
||||||
|
scripts
|
||||||
|
|||||||
+7998
-441
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,7 +1,7 @@
|
|||||||
# Spec Tasks Index
|
# Spec Tasks Index
|
||||||
|
|
||||||
> ⚠️ This file is generated. Do not edit manually.
|
> ⚠️ This file is generated. Do not edit manually.
|
||||||
> Generated at (UTC): `2026-03-21T07:05:32+00:00`
|
> Generated at (UTC): `2026-03-25T18:23:56+00:00`
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -28,9 +28,11 @@
|
|||||||
| 018 | DONE | 2026-03-16 | Add optional parcel type filter to price request | `spec/tasks/018_add_optional_parcel_type_filter_to_price_request.md` |
|
| 018 | DONE | 2026-03-16 | Add optional parcel type filter to price request | `spec/tasks/018_add_optional_parcel_type_filter_to_price_request.md` |
|
||||||
| 019 | TODO | 2026-03-14 | Add CDEK order creation endpoint | `spec/tasks/019_add_cdek_order_creation_endpoint.md` |
|
| 019 | TODO | 2026-03-14 | Add CDEK order creation endpoint | `spec/tasks/019_add_cdek_order_creation_endpoint.md` |
|
||||||
| 020 | DONE | 2026-03-21 | Rename price request model and use cities_map for CDEK codes | `spec/tasks/020_rename_price_request_model_and_use_cities_map_for_cdek_codes.md` |
|
| 020 | DONE | 2026-03-21 | Rename price request model and use cities_map for CDEK codes | `spec/tasks/020_rename_price_request_model_and_use_cities_map_for_cdek_codes.md` |
|
||||||
|
| 021 | TODO | 2026-03-25 | Add address suggestion adapter and country provider mapping | `spec/tasks/021_add_address_suggestion_adapter_and_country_mapping.md` |
|
||||||
|
| 022 | TODO | 2026-03-25 | Add address suggestion endpoint | `spec/tasks/022_add_address_suggestion_endpoint.md` |
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- Total: **21**
|
- Total: **23**
|
||||||
- TODO: **1**
|
- TODO: **3**
|
||||||
- DONE: **20**
|
- DONE: **20**
|
||||||
|
|||||||
+41
-1
@@ -17,7 +17,10 @@
|
|||||||
|
|
||||||
- Принимать запрос на расчёт стоимости доставки (идентификаторы городов отправления/назначения, вес, габариты)
|
- Принимать запрос на расчёт стоимости доставки (идентификаторы городов отправления/назначения, вес, габариты)
|
||||||
- Поддерживать необязательный параметр `parcel_type` в запросе расчёта стоимости доставки для фильтрации тарифов по типу отправления
|
- Поддерживать необязательный параметр `parcel_type` в запросе расчёта стоимости доставки для фильтрации тарифов по типу отправления
|
||||||
|
- Предоставлять отдельный endpoint подсказок адреса, чтобы frontend мог получить точное значение для `from_location.address` и `to_location.address` перед созданием заказа
|
||||||
- Принимать запрос на создание заказа CDEK по контракту из `http-client.http` для сценария "доставка, до двери"
|
- Принимать запрос на создание заказа CDEK по контракту из `http-client.http` для сценария "доставка, до двери"
|
||||||
|
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
|
||||||
|
- Для стран, сопоставленных с provider id `dadata`, использовать `dadata.ru`; конфигурация и wiring должны допускать отдельный address suggestion provider для европейских стран
|
||||||
- Опрашивать всех зарегистрированных провайдеров параллельно
|
- Опрашивать всех зарегистрированных провайдеров параллельно
|
||||||
- Возвращать унифицированный список тарифов, отсортированных по цене
|
- Возвращать унифицированный список тарифов, отсортированных по цене
|
||||||
- Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
|
- Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
|
||||||
@@ -44,10 +47,11 @@
|
|||||||
|
|
||||||
### Controller (`app/controllers/v1/delivery.py`)
|
### Controller (`app/controllers/v1/delivery.py`)
|
||||||
- `POST /api/v1/delivery/price` — принимает `DeliveryCalculationRequest`, возвращает `list[DeliveryPrice]`
|
- `POST /api/v1/delivery/price` — принимает `DeliveryCalculationRequest`, возвращает `list[DeliveryPrice]`
|
||||||
|
- `POST /api/v1/delivery/address/suggest` — принимает `AddressSuggestRequest`, возвращает `list[AddressSuggestion]`
|
||||||
- `POST /api/v1/delivery/order` — принимает `OrderCreateRequest`, возвращает `OrderCreateResponse`
|
- `POST /api/v1/delivery/order` — принимает `OrderCreateRequest`, возвращает `OrderCreateResponse`
|
||||||
- Парсинг и валидация входных данных через Pydantic
|
- Парсинг и валидация входных данных через Pydantic
|
||||||
- Маппинг исключений сервиса в HTTP-ответы
|
- Маппинг исключений сервиса в HTTP-ответы
|
||||||
- Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()` или `AggregatorService.create_order()`
|
- Каждый endpoint вызывает ровно один метод Service: `AggregatorService.get_all_prices()`, `AggregatorService.suggest_addresses()` или `AggregatorService.create_order()`
|
||||||
|
|
||||||
### Service (`app/services/aggregator.py`)
|
### Service (`app/services/aggregator.py`)
|
||||||
- `AggregatorService.get_all_prices(request: DeliveryCalculationRequest) -> list[DeliveryPrice]`
|
- `AggregatorService.get_all_prices(request: DeliveryCalculationRequest) -> list[DeliveryPrice]`
|
||||||
@@ -55,6 +59,8 @@
|
|||||||
- Принимает от каждого провайдера список тарифов и объединяет их в единый список
|
- Принимает от каждого провайдера список тарифов и объединяет их в единый список
|
||||||
- Фильтрует упавшие результаты
|
- Фильтрует упавшие результаты
|
||||||
- Сортирует тарифы по цене
|
- Сортирует тарифы по цене
|
||||||
|
- `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(request: OrderCreateRequest) -> OrderCreateResponse`
|
||||||
- `AggregatorService.create_order()` оркестрирует регистрацию заказа в CDEK через injected adapter dependency
|
- `AggregatorService.create_order()` оркестрирует регистрацию заказа в CDEK через injected adapter dependency
|
||||||
- Service не содержит бизнес-логики и provider HTTP-деталей
|
- Service не содержит бизнес-логики и provider HTTP-деталей
|
||||||
@@ -89,6 +95,18 @@
|
|||||||
|
|
||||||
Для сценария создания заказа CDEK adapter принимает валидированную order model, отправляет контракт `Регистрация заказа (тип "доставка", до двери)` из `http-client.http` и возвращает внутреннюю response model без утечки HTTP-деталей в Service.
|
Для сценария создания заказа 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
|
## Reference Data
|
||||||
@@ -123,6 +141,21 @@ delivery_days_min: int
|
|||||||
delivery_days_max: int
|
delivery_days_max: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Входная: `AddressSuggestRequest`
|
||||||
|
```
|
||||||
|
country_code: str
|
||||||
|
city: str
|
||||||
|
query: str
|
||||||
|
limit: int | None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Выходная: `AddressSuggestion`
|
||||||
|
```
|
||||||
|
provider: str
|
||||||
|
address: str
|
||||||
|
postal_code: str | None
|
||||||
|
```
|
||||||
|
|
||||||
### Входная: `OrderCreateRequest`
|
### Входная: `OrderCreateRequest`
|
||||||
```
|
```
|
||||||
type: Literal[2]
|
type: Literal[2]
|
||||||
@@ -149,6 +182,8 @@ services: list[{code: str, parameter: str}]
|
|||||||
packages: list[{number: str, weight: int, length: int, width: int, height: int, comment: str | None}]
|
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`
|
### Выходная: `OrderCreateResponse`
|
||||||
```
|
```
|
||||||
provider: str
|
provider: str
|
||||||
@@ -183,6 +218,10 @@ app/
|
|||||||
├── domain/
|
├── domain/
|
||||||
│ └── price.py # Business Logic
|
│ └── price.py # Business Logic
|
||||||
├── adapters/
|
├── adapters/
|
||||||
|
│ ├── address_suggestions/
|
||||||
|
│ │ ├── base.py # Интерфейс Adapter
|
||||||
|
│ │ └── dadata/
|
||||||
|
│ │ └── client.py
|
||||||
│ └── delivery_providers/
|
│ └── delivery_providers/
|
||||||
│ ├── base.py # Интерфейс Adapter
|
│ ├── base.py # Интерфейс Adapter
|
||||||
│ └── cdek/
|
│ └── cdek/
|
||||||
@@ -194,6 +233,7 @@ app/
|
|||||||
│ └── cache/
|
│ └── cache/
|
||||||
│ └── redis_cache.py # Repository
|
│ └── redis_cache.py # Repository
|
||||||
├── schemas/
|
├── schemas/
|
||||||
|
│ ├── address.py
|
||||||
│ ├── request.py
|
│ ├── request.py
|
||||||
│ ├── response.py
|
│ ├── response.py
|
||||||
│ └── order.py
|
│ └── order.py
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ created: 2026-03-14
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
Сейчас API поддерживает только `POST /api/v1/delivery/price`. Для нового пользовательского сценария нужен отдельный endpoint `/order`, который создаёт заказ в CDEK по контракту из `http-client.http`.
|
Сейчас order flow должен принимать уже готовые точные адреса в полях `from_location.address` и `to_location.address`. Для нового пользовательского сценария нужен отдельный endpoint `/order`, который создаёт заказ в CDEK по контракту из `http-client.http`, не выполняя address suggestion lookup внутри order flow.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
Добавить `POST /api/v1/delivery/order` в существующий controller и существующий service с request/response schemas и error mapping для регистрации заказа в CDEK через adapter contract из задачи `016`.
|
Добавить `POST /api/v1/delivery/order` в существующий controller и существующий service с request/response schemas и error mapping для регистрации заказа в CDEK через adapter contract из задачи `016`.
|
||||||
@@ -17,9 +17,10 @@ created: 2026-03-14
|
|||||||
- Новый endpoint должен быть добавлен в существующий controller модуль `app/controllers/v1/delivery.py`; не создавать отдельный controller модуль.
|
- Новый endpoint должен быть добавлен в существующий controller модуль `app/controllers/v1/delivery.py`; не создавать отдельный controller модуль.
|
||||||
- Логика создания заказа должна быть добавлена в существующий service модуль `app/services/aggregator.py`; не создавать отдельный service модуль.
|
- Логика создания заказа должна быть добавлена в существующий service модуль `app/services/aggregator.py`; не создавать отдельный service модуль.
|
||||||
- Service оркестрирует только вызов injected CDEK order adapter и не содержит business logic или provider HTTP-деталей.
|
- Service оркестрирует только вызов injected CDEK order adapter и не содержит business logic или provider HTTP-деталей.
|
||||||
|
- `from_location.address` и `to_location.address` считаются уже выбранными точными строками адреса; в рамках этой задачи запрещено добавлять address suggestion routing, внешние address lookup вызовы и нормализацию адреса.
|
||||||
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
||||||
- В scope задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
- В scope задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
||||||
- Scope задачи не включает кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
- Scope задачи не включает `POST /api/v1/delivery/address/suggest`, конфигурацию address suggestion providers, кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
||||||
- Не изменять файлы в `spec/`.
|
- Не изменять файлы в `spec/`.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
@@ -29,6 +30,7 @@ created: 2026-03-14
|
|||||||
- Controller делегирует обработку только в `AggregatorService.create_order()`.
|
- Controller делегирует обработку только в `AggregatorService.create_order()`.
|
||||||
- Service вызывает injected adapter для регистрации заказа и возвращает `OrderCreateResponse` с `provider` и `order_uuid`.
|
- Service вызывает injected adapter для регистрации заказа и возвращает `OrderCreateResponse` с `provider` и `order_uuid`.
|
||||||
- Логика orchestration размещена в существующем service `app/services/aggregator.py`.
|
- Логика orchestration размещена в существующем service `app/services/aggregator.py`.
|
||||||
|
- Endpoint принимает значения `from_location.address` и `to_location.address` как opaque input strings и не выполняет address suggestion lookup перед вызовом adapter.
|
||||||
- Ошибки валидации входного payload возвращают 422, provider request errors маппятся в 400, недоступность CDEK и transport failures — в 503.
|
- Ошибки валидации входного payload возвращают 422, provider request errors маппятся в 400, недоступность CDEK и transport failures — в 503.
|
||||||
- API и service tests покрывают success case и основные failure scenarios.
|
- API и service tests покрывают success case и основные failure scenarios.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
id: 021
|
||||||
|
title: Add address suggestion adapter and country provider mapping
|
||||||
|
status: TODO
|
||||||
|
created: 2026-03-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Перед созданием заказа клиенту нужно получить точное значение адреса для полей `from_location.address` и `to_location.address`. Для этого нужен отдельный address suggestion flow с внешним provider, а выбор provider должен определяться по `country_code` через YAML-конфиг.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Добавить contract для address suggestion adapters, секцию конфигурации address suggestions с маппингом `country_code -> provider_id` и реализовать интеграцию с `dadata.ru` для стран, сопоставленных с provider id `dadata`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- Изменения ограничены Adapter layer, config schema/loading и моделями, необходимыми для стабильного adapter contract.
|
||||||
|
- Внешний IO должен оставаться внутри address suggestion adapter modules.
|
||||||
|
- В scope задачи входит только интеграция с `dadata.ru`; concrete HTTP integration второго европейского provider не реализовывать в рамках этой задачи.
|
||||||
|
- Конфигурация должна читаться из `.yaml` и содержать отдельную секцию для address suggestion providers и country mapping.
|
||||||
|
- Adapter не должен содержать business decisions; он только отправляет запрос, маппит ответ и детерминированно обрабатывает provider/transport errors.
|
||||||
|
- Не изменять файлы в `spec/`.
|
||||||
|
- Поищи описание api dadata и следуй её описанию при составлении payload запроса
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
- Существует интерфейс `AddressSuggestionProvider` со стабильным контрактом `suggest(request: AddressSuggestRequest) -> list[AddressSuggestion]`.
|
||||||
|
- В YAML-конфиге добавлена секция address suggestions с настройками `dadata` и маппингом `country_code -> provider_id`.
|
||||||
|
- Реализован adapter `dadata`, который принимает internal request model и возвращает унифицированный список `AddressSuggestion`.
|
||||||
|
- Provider-specific поля ответа `dadata` не утекают за пределы adapter contract.
|
||||||
|
- Ошибки `dadata` класса 4xx маппятся в детерминированную provider request error, а transport/5xx ошибки — в adapter client error.
|
||||||
|
- Тесты покрывают загрузку конфигурации address suggestions, request/response mapping `dadata` и основные error scenarios.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- [ ] Добавлен base contract для address suggestion providers.
|
||||||
|
- [ ] Добавлена YAML-конфигурация address suggestions и country mapping.
|
||||||
|
- [ ] Реализован adapter `dadata` для address suggestions.
|
||||||
|
- [ ] Добавлены config и adapter tests для нового flow.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Обновить `tests/config/test_config_sections.py` для проверки секции address suggestions и маппинга стран на provider id.
|
||||||
|
- Добавить `tests/adapters/address_suggestions/dadata/test_client.py` для success/error сценариев и mapping.
|
||||||
|
- Проверить, что наружу возвращается только унифицированная model `AddressSuggestion`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||||
|
- `poetry run pytest tests/adapters/address_suggestions/dadata/test_client.py -q`
|
||||||
|
- `python3 spec/gen_spec_index.py --check`
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
id: 022
|
||||||
|
title: Add address suggestion endpoint
|
||||||
|
status: TODO
|
||||||
|
created: 2026-03-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Перед реализацией order flow клиенту нужен отдельный endpoint, который возвращает подсказки адреса и позволяет выбрать точное значение для `from_location.address` и `to_location.address`. Выбор provider должен происходить по `country_code` через конфигурационный маппинг стран.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Добавить `POST /api/v1/delivery/address/suggest` в существующий controller и существующий service с request/response schemas, routing на address suggestion provider по `country_code` и детерминированным HTTP error mapping.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
||||||
|
- Endpoint должен вызывать ровно один метод Service: `AggregatorService.suggest_addresses()`.
|
||||||
|
- Новый endpoint должен быть добавлен в существующий controller модуль `app/controllers/v1/delivery.py`; не создавать отдельный controller модуль.
|
||||||
|
- Логика provider selection должна быть добавлена в существующий service модуль `app/services/aggregator.py`; не создавать отдельный service модуль.
|
||||||
|
- Service выбирает provider по `country_code` через injected config mapping и вызывает ровно один address suggestion adapter; provider HTTP-детали в Service запрещены.
|
||||||
|
- Scope задачи не включает создание заказа CDEK, изменение `POST /api/v1/delivery/order`, расчёт стоимости доставки, cache behavior и concrete HTTP integration европейского provider.
|
||||||
|
- Не изменять файлы в `spec/`.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
- Существует endpoint `POST /api/v1/delivery/address/suggest`, принимающий `AddressSuggestRequest` и возвращающий `list[AddressSuggestion]`.
|
||||||
|
- Реализованы request/response schemas `AddressSuggestRequest` и `AddressSuggestion`.
|
||||||
|
- Endpoint реализован в существующем controller `app/controllers/v1/delivery.py`.
|
||||||
|
- Controller делегирует обработку только в `AggregatorService.suggest_addresses()`.
|
||||||
|
- Service определяет provider по `country_code` через конфигурационный маппинг и вызывает только соответствующий registered adapter.
|
||||||
|
- Если `country_code` отсутствует в маппинге или сопоставлен с незарегистрированным provider, endpoint возвращает детерминированный 400 response.
|
||||||
|
- Provider request errors маппятся в 400, недоступность внешнего сервиса и transport failures — в 503.
|
||||||
|
- Service и controller/API tests покрывают как минимум сценарии: route в `dadata`, route в второй provider через test double, unsupported country и provider failure.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- [ ] Добавлены address suggestion request/response schemas.
|
||||||
|
- [ ] Реализован метод `AggregatorService.suggest_addresses()` для provider routing и orchestration.
|
||||||
|
- [ ] Реализован endpoint `POST /api/v1/delivery/address/suggest` в существующем controller.
|
||||||
|
- [ ] Добавлены service и controller/API tests для address suggestion flow.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Добавить `tests/services/test_address_suggestions.py` для проверки routing по `country_code`, unsupported country, незарегистрированного provider и provider failures.
|
||||||
|
- Добавить `tests/controllers/v1/test_address_suggestions.py` для success case, schema validation и HTTP mapping ошибок.
|
||||||
|
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки подключения нового endpoint и service wiring без новых controller/service модулей.
|
||||||
|
- Использовать test doubles для address suggestion adapters.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- `poetry run pytest tests/services/test_address_suggestions.py -q`
|
||||||
|
- `poetry run pytest tests/controllers/v1/test_address_suggestions.py -q`
|
||||||
|
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||||
|
- `python3 spec/gen_spec_index.py --check`
|
||||||
Reference in New Issue
Block a user