fix order form

This commit is contained in:
Раис Юсупалиев
2026-04-11 00:36:37 +03:00
parent 18f7e251e3
commit ab0b66e1c2
9 changed files with 269 additions and 36 deletions
+3 -2
View File
@@ -32,9 +32,10 @@
| 023 | DONE | 2026-03-29 | Add Yandex Geosuggest address suggestion adapter and CIS routing | `spec/tasks/023_add_yandex_geosuggest_address_suggestion_adapter.md` |
| 024 | DONE | 2026-03-29 | Add TomTom address suggestion adapter and Europe routing | `spec/tasks/024_add_tomtom_address_suggestion_adapter.md` |
| 025 | DONE | 2026-04-03 | Remove company from Create Delivery Order parties | `spec/tasks/025_remove_company_from_create_delivery_order.md` |
| 026 | DONE | 2026-04-05 | Align CDEK order contract with single phone and kilogram package weight | `spec/tasks/026_align_cdek_order_contract_single_phone_and_weight_units.md` |
## Summary
- Total: **26**
- Total: **27**
- TODO: **0**
- DONE: **26**
- DONE: **27**
+8 -3
View File
@@ -97,6 +97,9 @@
- Для расчёта тарифа CDEK adapter принимает city identifiers из `DeliveryCalculationRequest`, находит запись в `cities_map`, берёт `cdek.code` и передаёт его в CDEK API
Для сценария создания заказа CDEK adapter принимает валидированную order model, отправляет контракт `Регистрация заказа (тип "доставка", до двери)` из `http-client.http` и возвращает внутреннюю response model без утечки HTTP-деталей в Service.
Во внутреннем order flow `sender` и `recipient` содержат ровно одно поле `phone`, а CDEK adapter сериализует его в provider payload `phones` с одним элементом.
Поле `services` в order flow является необязательным; при отсутствии значения adapter не отправляет `services` в CDEK payload.
Поле `packages[*].weight` во входном order request задаётся в килограммах, а CDEK adapter конвертирует его в граммы перед отправкой в provider API.
### Adapter (`app/adapters/address_suggestions`)
- `base.py` — абстрактный интерфейс `AddressSuggestionProvider`:
@@ -173,11 +176,11 @@ comment: str | None
sender:
name: str
email: str
phones: list[{number: str}]
phone: {number: str}
recipient:
name: str
email: str
phones: list[{number: str}]
phone: {number: str}
from_location:
address: str
city: str
@@ -186,11 +189,13 @@ to_location:
address: str
city: str
country_code: str
services: list[{code: str, parameter: str}]
services: list[{code: str, parameter: 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.
`sender.phone` и `recipient.phone` представляют единственный телефон для соответствующей стороны заказа; передача нескольких телефонов во входном API не поддерживается.
`packages[*].weight` в `OrderCreateRequest` задаётся в килограммах, а в payload CDEK должен передаваться в граммах.
### Выходная: `OrderCreateResponse`
```
@@ -0,0 +1,52 @@
---
id: 026
title: Align CDEK order contract with single phone and kilogram package weight
status: DONE
created: 2026-04-05
---
## Context
Текущий order flow принимает `sender.phones` и `recipient.phones` как список, требует обязательное поле `services` и пробрасывает `packages.weight` в CDEK payload без явной фиксации единиц измерения. Новый контракт должен принимать один телефон на сторону, разрешать отсутствие `services` и гарантировать, что во входном API вес упаковки задаётся в килограммах, а в CDEK отправляется в граммах.
## Goal
Обновить flow `POST /api/v1/delivery/order` по слоям Controller, Service и Adapter так, чтобы public/internal request contract использовал `phone` вместо `phones`, поле `services` было необязательным, а CDEK order mapper конвертировал `packages.weight` из килограммов во входном запросе в граммы во внешнем provider payload.
## Constraints
- Изменения ограничены существующими модулями order flow: `app/schemas/order.py`, `app/controllers/v1/delivery.py`, `app/services/aggregator.py`, `app/adapters/delivery_providers/cdek/`, `http-client.http` и связанными тестами.
- `POST /api/v1/delivery/order` должен оставаться в существующем controller и по-прежнему вызывать ровно один метод Service: `AggregatorService.create_order()`.
- Service остаётся orchestration layer и не получает новую business logic; он только принимает обновлённую order model и делегирует её adapter.
- Во внутреннем order flow и публичном API поле `phones` должно быть удалено; передача нескольких телефонов больше не поддерживается.
- CDEK adapter может сериализовать внутренний `phone` в provider-specific поле `phones`, если это требуется внешним контрактом CDEK, но множественность телефонов не должна возвращаться во внутренние модели и controller contract.
- `services` должно быть необязательным полем request schema; при отсутствии значения нельзя подставлять фиктивные service entries.
- Конвертация единиц `packages.weight` должна выполняться только на границе CDEK adapter mapping: входной order request использует килограммы, исходящий payload в CDEK использует граммы.
- Scope задачи не включает изменение response contract, price flow, address suggestion flow, provider routing, кеширование, новые провайдеры и расширение поддерживаемых `type`/`tariff_code`.
- Не изменять файлы в `spec/`.
## Acceptance criteria
- `OrderCreateRequest` использует `sender.phone` и `recipient.phone` вместо `sender.phones` и `recipient.phones`.
- Если request payload содержит `sender.phones` или `recipient.phones`, endpoint возвращает 422 на уровне schema validation.
- `POST /api/v1/delivery/order` принимает валидный payload без поля `services`.
- Если `services` отсутствует или равен `null`, service и adapter flow успешно обрабатывают запрос без добавления `services` в исходящий CDEK payload.
- `AggregatorService.create_order()` продолжает только оркестрировать вызов injected order adapter и не содержит преобразования `phone`/`phones` или килограммов в граммы.
- CDEK order mapper формирует provider payload с полями `sender.phones` и `recipient.phones`, каждое из которых содержит ровно один элемент, полученный из соответствующего внутреннего поля `phone`.
- Для каждого элемента `packages` исходящий payload CDEK содержит `weight`, равный значению входного `packages.weight`, умноженному на `1000`.
- `http-client.http` содержит актуальный пример Create Delivery Order с `phone` вместо `phones`, без обязательного `services` и с весом, отражающим controller contract в килограммах.
## Definition of Done
- [ ] Обновлён public/internal order request contract на `phone` вместо `phones`.
- [ ] `services` сделано необязательным без изменения endpoint path и service orchestration.
- [ ] Реализован mapping одного телефона в provider payload `phones` и конвертация `packages.weight` из килограммов в граммы.
- [ ] Обновлены controller, service и adapter tests под новый контракт и unit conversion.
- [ ] Обновлён пример запроса в `http-client.http`.
## Tests
- Обновить `tests/controllers/v1/test_order.py` для success case с `phone`, сценариев 422 при передаче `sender.phones` и `recipient.phones`, а также для запроса без `services`.
- Обновить `tests/services/test_order.py` для проверки, что service принимает обновлённую order model, делегирует её adapter без новой логики и корректно работает с `services=None`.
- Обновить `tests/adapters/delivery_providers/cdek/test_order_client.py` для проверки mapping `phone -> phones[0]`, отсутствия `services` в исходящем payload при `None` и конвертации `packages.weight` из килограммов в граммы.
- При необходимости обновить другие order-related tests и fixtures, завязанные на старые поля `phones` и обязательность `services`.
## Commands
- `poetry run pytest tests/controllers/v1/test_order.py -q`
- `poetry run pytest tests/services/test_order.py -q`
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_order_client.py -q`
- `python3 spec/gen_spec_index.py --check`