017 add creating order router
This commit is contained in:
@@ -6,12 +6,14 @@ from app.adapters.delivery_providers.cdek import CDEKProvider
|
|||||||
from app.config import Settings
|
from app.config import Settings
|
||||||
from app.controllers.http_client import build_controller_http_client
|
from app.controllers.http_client import build_controller_http_client
|
||||||
from app.repositories.cache.redis_cache import PriceCache
|
from app.repositories.cache.redis_cache import PriceCache
|
||||||
|
from app.schemas.order import OrderCreateRequest, OrderCreateResponse
|
||||||
from app.schemas.request import DeliveryRequest
|
from app.schemas.request import DeliveryRequest
|
||||||
from app.schemas.response import DeliveryPrice
|
from app.schemas.response import DeliveryPrice
|
||||||
from app.services.aggregator import (
|
from app.services.aggregator import (
|
||||||
AggregatorService,
|
AggregatorService,
|
||||||
AggregatorServiceError,
|
AggregatorServiceError,
|
||||||
InvalidDeliveryRequestError,
|
InvalidDeliveryRequestError,
|
||||||
|
InvalidOrderCreateRequestError,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/delivery", tags=["delivery"])
|
router = APIRouter(prefix="/delivery", tags=["delivery"])
|
||||||
@@ -24,16 +26,16 @@ def _build_aggregator_service(settings: Settings) -> AggregatorService:
|
|||||||
http_client = build_controller_http_client(
|
http_client = build_controller_http_client(
|
||||||
timeout_seconds=settings.adapter.cdek_timeout_seconds
|
timeout_seconds=settings.adapter.cdek_timeout_seconds
|
||||||
)
|
)
|
||||||
providers = (
|
cdek_provider = CDEKProvider.from_adapter_config(
|
||||||
CDEKProvider.from_adapter_config(
|
http_client=http_client,
|
||||||
http_client=http_client,
|
adapter_config=settings.adapter,
|
||||||
adapter_config=settings.adapter,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
providers = (cdek_provider,)
|
||||||
cache = PriceCache.from_repository_config(settings.repository)
|
cache = PriceCache.from_repository_config(settings.repository)
|
||||||
service = AggregatorService(
|
service = AggregatorService(
|
||||||
providers=providers,
|
providers=providers,
|
||||||
cache=cache,
|
cache=cache,
|
||||||
|
order_adapter=cdek_provider,
|
||||||
weight_round_scale=settings.business_logic.weight_round_scale,
|
weight_round_scale=settings.business_logic.weight_round_scale,
|
||||||
provider_price_multiplier=settings.business_logic.provider_price_multiplier,
|
provider_price_multiplier=settings.business_logic.provider_price_multiplier,
|
||||||
)
|
)
|
||||||
@@ -77,3 +79,31 @@ async def get_delivery_price(
|
|||||||
"message": "Delivery price aggregation is temporarily unavailable.",
|
"message": "Delivery price aggregation is temporarily unavailable.",
|
||||||
},
|
},
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/order",
|
||||||
|
response_model=OrderCreateResponse,
|
||||||
|
)
|
||||||
|
async def create_delivery_order(
|
||||||
|
order_request: OrderCreateRequest,
|
||||||
|
service: AggregatorService = Depends(get_aggregator_service),
|
||||||
|
) -> OrderCreateResponse:
|
||||||
|
try:
|
||||||
|
return await service.create_order(order_request)
|
||||||
|
except InvalidOrderCreateRequestError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail={
|
||||||
|
"code": "invalid_order_create_request",
|
||||||
|
"message": "Order request contains invalid or unsupported CDEK data.",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
except AggregatorServiceError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail={
|
||||||
|
"code": "order_creation_unavailable",
|
||||||
|
"message": "CDEK order creation is temporarily unavailable.",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from app.domain.price import (
|
|||||||
filter_and_sort_prices,
|
filter_and_sort_prices,
|
||||||
normalize_delivery_request,
|
normalize_delivery_request,
|
||||||
)
|
)
|
||||||
|
from app.schemas.order import OrderCreateRequest, OrderCreateResponse
|
||||||
from app.schemas.request import DeliveryRequest
|
from app.schemas.request import DeliveryRequest
|
||||||
from app.schemas.response import DeliveryPrice
|
from app.schemas.response import DeliveryPrice
|
||||||
|
|
||||||
@@ -27,6 +28,14 @@ class InvalidDeliveryRequestError(AggregatorServiceError):
|
|||||||
"""Raised when provider rejects delivery request as invalid."""
|
"""Raised when provider rejects delivery request as invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidOrderCreateRequestError(AggregatorServiceError):
|
||||||
|
"""Raised when provider rejects order creation payload as invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCreationUnavailableError(AggregatorServiceError):
|
||||||
|
"""Raised when order creation cannot be completed."""
|
||||||
|
|
||||||
|
|
||||||
class PriceCacheProtocol(Protocol):
|
class PriceCacheProtocol(Protocol):
|
||||||
async def get(self, key: str) -> object | None: ...
|
async def get(self, key: str) -> object | None: ...
|
||||||
|
|
||||||
@@ -42,11 +51,18 @@ class FilterAndSortPricesFn(Protocol):
|
|||||||
) -> list[object]: ...
|
) -> list[object]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class OrderRegistrationAdapterProtocol(Protocol):
|
||||||
|
async def register_order(
|
||||||
|
self, request: OrderCreateRequest
|
||||||
|
) -> OrderCreateResponse: ...
|
||||||
|
|
||||||
|
|
||||||
class AggregatorService:
|
class AggregatorService:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
providers: Sequence[DeliveryProvider],
|
providers: Sequence[DeliveryProvider],
|
||||||
cache: PriceCacheProtocol | None = None,
|
cache: PriceCacheProtocol | None = None,
|
||||||
|
order_adapter: OrderRegistrationAdapterProtocol | None = None,
|
||||||
*,
|
*,
|
||||||
weight_round_scale: int = DEFAULT_WEIGHT_ROUND_SCALE,
|
weight_round_scale: int = DEFAULT_WEIGHT_ROUND_SCALE,
|
||||||
provider_price_multiplier: Decimal = DEFAULT_PROVIDER_PRICE_MULTIPLIER,
|
provider_price_multiplier: Decimal = DEFAULT_PROVIDER_PRICE_MULTIPLIER,
|
||||||
@@ -54,6 +70,7 @@ class AggregatorService:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._providers = tuple(providers)
|
self._providers = tuple(providers)
|
||||||
self._cache = cache
|
self._cache = cache
|
||||||
|
self._order_adapter = order_adapter
|
||||||
self._weight_round_scale = weight_round_scale
|
self._weight_round_scale = weight_round_scale
|
||||||
self._provider_price_multiplier = provider_price_multiplier
|
self._provider_price_multiplier = provider_price_multiplier
|
||||||
self._filter_and_sort_prices = filter_and_sort_prices_fn
|
self._filter_and_sort_prices = filter_and_sort_prices_fn
|
||||||
@@ -103,6 +120,28 @@ class AggregatorService:
|
|||||||
)
|
)
|
||||||
return [self._coerce_delivery_price(price) for price in filtered_and_sorted]
|
return [self._coerce_delivery_price(price) for price in filtered_and_sorted]
|
||||||
|
|
||||||
|
async def create_order(self, request: OrderCreateRequest) -> OrderCreateResponse:
|
||||||
|
if self._order_adapter is None:
|
||||||
|
raise OrderCreationUnavailableError(
|
||||||
|
"Order registration adapter is not configured."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
created_order = await self._order_adapter.register_order(request)
|
||||||
|
except ProviderRequestError as exc:
|
||||||
|
raise InvalidOrderCreateRequestError(
|
||||||
|
"Order request is invalid for the configured provider."
|
||||||
|
) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
raise OrderCreationUnavailableError(
|
||||||
|
"Order creation is temporarily unavailable."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return OrderCreateResponse.model_validate(
|
||||||
|
created_order,
|
||||||
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
|
||||||
async def _get_provider_price(
|
async def _get_provider_price(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
+1
-1
@@ -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-14T00:55:12+00:00`
|
> Generated at (UTC): `2026-03-14T00:57:33+00:00`
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ delivery_days_max: int
|
|||||||
### Входная: `OrderCreateRequest`
|
### Входная: `OrderCreateRequest`
|
||||||
```
|
```
|
||||||
type: Literal[2]
|
type: Literal[2]
|
||||||
tariff_code: Literal[136]
|
tariff_code: Literal[535]
|
||||||
comment: str | None
|
comment: str | None
|
||||||
sender:
|
sender:
|
||||||
company: str | None
|
company: str | None
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ created: 2026-03-14
|
|||||||
Сейчас API поддерживает только `POST /api/v1/delivery/price`. Для нового пользовательского сценария нужен отдельный endpoint `/order`, который создаёт заказ в CDEK по контракту из `http-client.http`.
|
Сейчас API поддерживает только `POST /api/v1/delivery/price`. Для нового пользовательского сценария нужен отдельный endpoint `/order`, который создаёт заказ в CDEK по контракту из `http-client.http`.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
Добавить `POST /api/v1/order` в существующий controller и существующий service с request/response schema и error mapping для регистрации заказа в CDEK через adapter contract из задачи `016`.
|
Добавить `POST /api/v1/delivery/order` в существующий controller и существующий service с request/response schemas и error mapping для регистрации заказа в CDEK через adapter contract из задачи `016`.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
||||||
@@ -18,15 +18,16 @@ created: 2026-03-14
|
|||||||
- Логика создания заказа должна быть добавлена в существующий 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-деталей.
|
||||||
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
||||||
|
- В scope задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
||||||
- Scope задачи не включает кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
- Scope задачи не включает кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
||||||
- Не изменять файлы в `spec/`.
|
- Не изменять файлы в `spec/`.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
- Существует endpoint `POST /api/v1/order`, принимающий payload по контракту из `http-client.http`.
|
- Существует endpoint `POST /api/v1/delivery/order`, принимающий payload по контракту из `http-client.http`.
|
||||||
- Реализованы request/response schemas для создания заказа и минимального успешного ответа API.
|
- Реализованы request/response schemas `OrderCreateRequest` и `OrderCreateResponse` для создания заказа.
|
||||||
- Endpoint реализован в существующем controller `app/controllers/v1/delivery.py`.
|
- Endpoint реализован в существующем controller `app/controllers/v1/delivery.py`.
|
||||||
- Controller делегирует обработку только в `AggregatorService.create_order()`.
|
- Controller делегирует обработку только в `AggregatorService.create_order()`.
|
||||||
- Service вызывает injected adapter для регистрации заказа и возвращает response model с `order_uuid`.
|
- Service вызывает injected adapter для регистрации заказа и возвращает `OrderCreateResponse` с `provider` и `order_uuid`.
|
||||||
- Логика orchestration размещена в существующем service `app/services/aggregator.py`.
|
- Логика orchestration размещена в существующем service `app/services/aggregator.py`.
|
||||||
- Ошибки валидации входного 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.
|
||||||
@@ -34,7 +35,7 @@ created: 2026-03-14
|
|||||||
## Definition of Done
|
## Definition of Done
|
||||||
- [ ] Добавлены order request/response schemas.
|
- [ ] Добавлены order request/response schemas.
|
||||||
- [ ] Реализован метод `AggregatorService.create_order()` для оркестрации регистрации заказа.
|
- [ ] Реализован метод `AggregatorService.create_order()` для оркестрации регистрации заказа.
|
||||||
- [ ] Реализован endpoint `POST /api/v1/order` в существующем controller.
|
- [ ] Реализован endpoint `POST /api/v1/delivery/order` в существующем controller.
|
||||||
- [ ] Добавлены service и controller/API tests для order creation flow.
|
- [ ] Добавлены service и controller/API tests для order creation flow.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.controllers.v1.delivery import get_aggregator_service
|
||||||
|
from app.main import create_app
|
||||||
|
from app.schemas.order import OrderCreateRequest, OrderCreateResponse
|
||||||
|
from app.services.aggregator import (
|
||||||
|
AggregatorServiceError,
|
||||||
|
InvalidOrderCreateRequestError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StubAggregatorService:
|
||||||
|
def __init__(self, *, response: object, error: Exception | None = None) -> None:
|
||||||
|
self._response = response
|
||||||
|
self._error = error
|
||||||
|
self.calls: list[OrderCreateRequest] = []
|
||||||
|
|
||||||
|
async def create_order(self, request: OrderCreateRequest) -> object:
|
||||||
|
self.calls.append(request)
|
||||||
|
if self._error is not None:
|
||||||
|
raise self._error
|
||||||
|
return self._response
|
||||||
|
|
||||||
|
|
||||||
|
def _install_service_override(app, service: StubAggregatorService) -> None:
|
||||||
|
async def override_service() -> StubAggregatorService:
|
||||||
|
return service
|
||||||
|
|
||||||
|
app.dependency_overrides[get_aggregator_service] = override_service
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_payload() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"type": 2,
|
||||||
|
"tariff_code": 535,
|
||||||
|
"comment": "Test order",
|
||||||
|
"sender": {
|
||||||
|
"company": "Romashka LLC",
|
||||||
|
"name": "Petr Petrov",
|
||||||
|
"email": "sender@example.com",
|
||||||
|
"phones": [{"number": "+79009876543"}],
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"name": "Ivan Ivanov",
|
||||||
|
"email": "ivan@example.com",
|
||||||
|
"phones": [{"number": "+79001234567"}],
|
||||||
|
},
|
||||||
|
"from_location": {
|
||||||
|
"address": "Lenina 1",
|
||||||
|
"city": "Moscow",
|
||||||
|
"country_code": "RU",
|
||||||
|
},
|
||||||
|
"to_location": {
|
||||||
|
"address": "Pushkina 10",
|
||||||
|
"city": "Novosibirsk",
|
||||||
|
"country_code": "RU",
|
||||||
|
},
|
||||||
|
"services": [{"code": "INSURANCE", "parameter": "1000"}],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"number": "1",
|
||||||
|
"weight": 1000,
|
||||||
|
"length": 20,
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"comment": "Package 1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_delivery_order_returns_response_and_delegates_to_service() -> None:
|
||||||
|
expected_response = OrderCreateResponse(provider="cdek", order_uuid="order-uuid-1")
|
||||||
|
service = StubAggregatorService(response=expected_response)
|
||||||
|
app = create_app()
|
||||||
|
_install_service_override(app, service)
|
||||||
|
|
||||||
|
async def run_request() -> httpx.Response:
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
return await client.post("/api/v1/delivery/order", json=_valid_payload())
|
||||||
|
|
||||||
|
response = asyncio.run(run_request())
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == expected_response.model_dump(mode="json")
|
||||||
|
assert service.calls == [OrderCreateRequest(**_valid_payload())]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_delivery_order_rejects_invalid_payload() -> None:
|
||||||
|
service = StubAggregatorService(response=None)
|
||||||
|
app = create_app()
|
||||||
|
_install_service_override(app, service)
|
||||||
|
invalid_payload = _valid_payload()
|
||||||
|
invalid_payload["tariff_code"] = 136
|
||||||
|
|
||||||
|
async def run_request() -> httpx.Response:
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
return await client.post("/api/v1/delivery/order", json=invalid_payload)
|
||||||
|
|
||||||
|
response = asyncio.run(run_request())
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
assert service.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_delivery_order_maps_invalid_request_to_400() -> None:
|
||||||
|
service = StubAggregatorService(
|
||||||
|
response=None,
|
||||||
|
error=InvalidOrderCreateRequestError("invalid payload"),
|
||||||
|
)
|
||||||
|
app = create_app()
|
||||||
|
_install_service_override(app, service)
|
||||||
|
|
||||||
|
async def run_request() -> httpx.Response:
|
||||||
|
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
return await client.post("/api/v1/delivery/order", json=_valid_payload())
|
||||||
|
|
||||||
|
response = asyncio.run(run_request())
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": {
|
||||||
|
"code": "invalid_order_create_request",
|
||||||
|
"message": "Order request contains invalid or unsupported CDEK data.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_delivery_order_maps_service_exception_to_503() -> None:
|
||||||
|
service = StubAggregatorService(
|
||||||
|
response=None,
|
||||||
|
error=AggregatorServiceError("service unavailable"),
|
||||||
|
)
|
||||||
|
app = create_app()
|
||||||
|
_install_service_override(app, service)
|
||||||
|
|
||||||
|
async def run_request() -> httpx.Response:
|
||||||
|
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
return await client.post("/api/v1/delivery/order", json=_valid_payload())
|
||||||
|
|
||||||
|
response = asyncio.run(run_request())
|
||||||
|
|
||||||
|
assert response.status_code == 503
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": {
|
||||||
|
"code": "order_creation_unavailable",
|
||||||
|
"message": "CDEK order creation is temporarily unavailable.",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.adapters.delivery_providers.base import ProviderRequestError
|
||||||
|
from app.schemas.order import OrderCreateRequest, OrderCreateResponse
|
||||||
|
from app.services.aggregator import (
|
||||||
|
AggregatorService,
|
||||||
|
InvalidOrderCreateRequestError,
|
||||||
|
OrderCreationUnavailableError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StubOrderAdapter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
response: OrderCreateResponse | None = None,
|
||||||
|
error: Exception | None = None,
|
||||||
|
) -> None:
|
||||||
|
self._response = response
|
||||||
|
self._error = error
|
||||||
|
self.calls: list[OrderCreateRequest] = []
|
||||||
|
|
||||||
|
async def register_order(self, request: OrderCreateRequest) -> OrderCreateResponse:
|
||||||
|
self.calls.append(request)
|
||||||
|
if self._error is not None:
|
||||||
|
raise self._error
|
||||||
|
if self._response is None:
|
||||||
|
raise RuntimeError("Stub order adapter has no response configured.")
|
||||||
|
return self._response
|
||||||
|
|
||||||
|
|
||||||
|
def _make_order_request(**overrides: object) -> OrderCreateRequest:
|
||||||
|
payload: dict[str, object] = {
|
||||||
|
"type": 2,
|
||||||
|
"tariff_code": 535,
|
||||||
|
"comment": "Test order",
|
||||||
|
"sender": {
|
||||||
|
"company": "Romashka LLC",
|
||||||
|
"name": "Petr Petrov",
|
||||||
|
"email": "sender@example.com",
|
||||||
|
"phones": [{"number": "+79009876543"}],
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"name": "Ivan Ivanov",
|
||||||
|
"email": "ivan@example.com",
|
||||||
|
"phones": [{"number": "+79001234567"}],
|
||||||
|
},
|
||||||
|
"from_location": {
|
||||||
|
"address": "Lenina 1",
|
||||||
|
"city": "Moscow",
|
||||||
|
"country_code": "RU",
|
||||||
|
},
|
||||||
|
"to_location": {
|
||||||
|
"address": "Pushkina 10",
|
||||||
|
"city": "Novosibirsk",
|
||||||
|
"country_code": "RU",
|
||||||
|
},
|
||||||
|
"services": [{"code": "INSURANCE", "parameter": "1000"}],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"number": "1",
|
||||||
|
"weight": 1000,
|
||||||
|
"length": 20,
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"comment": "Package 1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
payload.update(overrides)
|
||||||
|
return OrderCreateRequest(**payload)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_order_delegates_to_adapter_and_returns_created_order() -> None:
|
||||||
|
request = _make_order_request()
|
||||||
|
adapter = StubOrderAdapter(
|
||||||
|
response=OrderCreateResponse(provider="cdek", order_uuid="order-uuid-1")
|
||||||
|
)
|
||||||
|
service = AggregatorService(providers=[], order_adapter=adapter)
|
||||||
|
|
||||||
|
result = asyncio.run(service.create_order(request))
|
||||||
|
|
||||||
|
assert result == OrderCreateResponse(provider="cdek", order_uuid="order-uuid-1")
|
||||||
|
assert adapter.calls == [request]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_order_maps_provider_request_errors_to_invalid_order_error() -> None:
|
||||||
|
request = _make_order_request()
|
||||||
|
adapter = StubOrderAdapter(error=ProviderRequestError("bad payload"))
|
||||||
|
service = AggregatorService(providers=[], order_adapter=adapter)
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderCreateRequestError):
|
||||||
|
asyncio.run(service.create_order(request))
|
||||||
|
|
||||||
|
assert adapter.calls == [request]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_order_maps_transport_failures_to_unavailable_error() -> None:
|
||||||
|
request = _make_order_request()
|
||||||
|
adapter = StubOrderAdapter(error=RuntimeError("transport down"))
|
||||||
|
service = AggregatorService(providers=[], order_adapter=adapter)
|
||||||
|
|
||||||
|
with pytest.raises(OrderCreationUnavailableError):
|
||||||
|
asyncio.run(service.create_order(request))
|
||||||
|
|
||||||
|
assert adapter.calls == [request]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_order_without_configured_adapter_raises_unavailable_error() -> None:
|
||||||
|
service = AggregatorService(providers=[])
|
||||||
|
|
||||||
|
with pytest.raises(OrderCreationUnavailableError):
|
||||||
|
asyncio.run(service.create_order(_make_order_request()))
|
||||||
Reference in New Issue
Block a user