diff --git a/alembic/versions/20260626_039_generalize_provider_order_state.py b/alembic/versions/20260626_039_generalize_provider_order_state.py new file mode 100644 index 0000000..31ccd9e --- /dev/null +++ b/alembic/versions/20260626_039_generalize_provider_order_state.py @@ -0,0 +1,115 @@ +"""Generalize provider order state columns.""" + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa + + +revision: str = "20260626_039" +down_revision: str | None = "20260530_038" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "orders", + sa.Column("provider_order_id", sa.String(length=128), nullable=True), + ) + op.add_column( + "orders", + sa.Column("provider_order_status", sa.String(length=64), nullable=True), + ) + op.add_column( + "orders", + sa.Column("provider_waybill_id", sa.String(length=128), nullable=True), + ) + op.add_column( + "orders", + sa.Column("provider_waybill_url", sa.String(length=2048), nullable=True), + ) + op.add_column( + "orders", + sa.Column("provider_polled_at", sa.DateTime(timezone=True), nullable=True), + ) + + op.execute( + """ + UPDATE orders + SET + provider_order_id = cdek_order_uuid, + provider_order_status = cdek_order_status, + provider_waybill_id = cdek_waybill_uuid, + provider_waybill_url = cdek_waybill_url, + provider_polled_at = cdek_polled_at + WHERE provider = 'cdek' + """ + ) + op.execute( + """ + UPDATE orders + SET provider_order_id = cse_order_number + WHERE provider = 'cse' + """ + ) + + op.drop_column("orders", "cse_order_number") + op.drop_column("orders", "cdek_polled_at") + op.drop_column("orders", "cdek_order_status") + op.drop_column("orders", "cdek_waybill_url") + op.drop_column("orders", "cdek_waybill_uuid") + op.drop_column("orders", "cdek_order_uuid") + + +def downgrade() -> None: + op.add_column( + "orders", + sa.Column("cdek_order_uuid", sa.String(length=128), nullable=True), + ) + op.add_column( + "orders", + sa.Column("cdek_waybill_uuid", sa.String(length=128), nullable=True), + ) + op.add_column( + "orders", + sa.Column("cdek_waybill_url", sa.String(length=2048), nullable=True), + ) + op.add_column( + "orders", + sa.Column("cdek_order_status", sa.String(length=64), nullable=True), + ) + op.add_column( + "orders", + sa.Column("cdek_polled_at", sa.DateTime(timezone=True), nullable=True), + ) + op.add_column( + "orders", + sa.Column("cse_order_number", sa.String(length=128), nullable=True), + ) + + op.execute( + """ + UPDATE orders + SET + cdek_order_uuid = provider_order_id, + cdek_order_status = provider_order_status, + cdek_waybill_uuid = provider_waybill_id, + cdek_waybill_url = provider_waybill_url, + cdek_polled_at = provider_polled_at + WHERE provider = 'cdek' + """ + ) + op.execute( + """ + UPDATE orders + SET cse_order_number = provider_order_id + WHERE provider = 'cse' + """ + ) + + op.drop_column("orders", "provider_polled_at") + op.drop_column("orders", "provider_waybill_url") + op.drop_column("orders", "provider_waybill_id") + op.drop_column("orders", "provider_order_status") + op.drop_column("orders", "provider_order_id") diff --git a/app/adapters/delivery_providers/base.py b/app/adapters/delivery_providers/base.py index 8d595ff..bee91e8 100644 --- a/app/adapters/delivery_providers/base.py +++ b/app/adapters/delivery_providers/base.py @@ -1,7 +1,9 @@ """Base interface for delivery providers.""" from abc import ABC, abstractmethod +from typing import Protocol, runtime_checkable +from app.schemas.payment import InitPaymentRequest from app.schemas.request import DeliveryCalculationRequest from app.schemas.response import DeliveryPrice @@ -18,6 +20,27 @@ class DeliveryProvider(ABC): raise NotImplementedError +@runtime_checkable +class PaymentPriceValidationProvider(Protocol): + name: str + + async def get_payment_price( + self, + request: InitPaymentRequest, + ) -> DeliveryPrice | None: ... + + +@runtime_checkable +class OrderRegistrationProvider(Protocol): + name: str + + async def register_order( + self, + request: InitPaymentRequest, + order_uuid: str, + ) -> object: ... + + class ProviderClientError(RuntimeError): """Raised when a provider call fails for temporary or provider-side reasons.""" diff --git a/app/adapters/delivery_providers/cdek/client.py b/app/adapters/delivery_providers/cdek/client.py index 9d38e6a..f8ba09a 100644 --- a/app/adapters/delivery_providers/cdek/client.py +++ b/app/adapters/delivery_providers/cdek/client.py @@ -33,7 +33,7 @@ from app.adapters.delivery_providers.cdek.order_mapper import ( resolve_cdek_city_code, ) from app.cities import cities_map -from app.config import AdapterConfig +from app.config import CDEKDeliveryProviderConfig from app.schemas.payment import InitPaymentRequest from app.schemas.request import DeliveryCalculationRequest from app.schemas.response import DeliveryPrice @@ -452,28 +452,28 @@ class CDEKProvider(DeliveryProvider): self.cache_ttl_seconds = cache_ttl_seconds @classmethod - def from_adapter_config( + def from_config( cls, *, http_client: httpx.AsyncClient, - adapter_config: AdapterConfig, + config: CDEKDeliveryProviderConfig, ) -> "CDEKProvider": auth_client = CDEKAuthClient( http_client=http_client, - base_url=adapter_config.cdek_base_url, - client_id=adapter_config.cdek_client_id, - client_secret=adapter_config.cdek_client_secret, - timeout_seconds=adapter_config.cdek_timeout_seconds, + base_url=config.base_url, + client_id=config.client_id, + client_secret=config.client_secret, + timeout_seconds=config.timeout_seconds, ) client = CDEKClient( http_client=http_client, auth_client=auth_client, - base_url=adapter_config.cdek_base_url, - timeout_seconds=adapter_config.cdek_timeout_seconds, - retry_attempts=adapter_config.cdek_retry_attempts, - retry_backoff_seconds=adapter_config.cdek_retry_backoff_seconds, + base_url=config.base_url, + timeout_seconds=config.timeout_seconds, + retry_attempts=config.retry_attempts, + retry_backoff_seconds=config.retry_backoff_seconds, ) - return cls(client=client, cache_ttl_seconds=adapter_config.cdek_cache_ttl_seconds) + return cls(client=client, cache_ttl_seconds=config.cache_ttl_seconds) async def get_prices( self, request: DeliveryCalculationRequest diff --git a/app/adapters/delivery_providers/cse/client.py b/app/adapters/delivery_providers/cse/client.py index 0274137..73c4915 100644 --- a/app/adapters/delivery_providers/cse/client.py +++ b/app/adapters/delivery_providers/cse/client.py @@ -35,7 +35,7 @@ from app.adapters.delivery_providers.cse.soap import ( make_field, parse_response, ) -from app.config import AdapterConfig +from app.config import CSEDeliveryProviderConfig from app.schemas.payment import InitPaymentRequest from app.schemas.request import DeliveryCalculationRequest from app.schemas.response import DeliveryPrice @@ -212,27 +212,27 @@ class CSEProvider(DeliveryProvider): self._delivery_types: list[tuple[str, str]] | None = None @classmethod - def from_adapter_config( + def from_config( cls, *, http_client: httpx.AsyncClient, - adapter_config: AdapterConfig, + config: CSEDeliveryProviderConfig, ) -> "CSEProvider": client = CSEClient( http_client=http_client, - base_url=adapter_config.cse_base_url, - login=adapter_config.cse_login, - password=adapter_config.cse_password, + base_url=config.base_url, + login=config.login, + password=config.password, registration_params=CSEOrderRegistrationParams( - payer=adapter_config.cse_payer, - payment_method=adapter_config.cse_payment_method, - shipping_method=adapter_config.cse_shipping_method, + payer=config.payer, + payment_method=config.payment_method, + shipping_method=config.shipping_method, ), - timeout_seconds=adapter_config.cse_timeout_seconds, - retry_attempts=adapter_config.cse_retry_attempts, - retry_backoff_seconds=adapter_config.cse_retry_backoff_seconds, + timeout_seconds=config.timeout_seconds, + retry_attempts=config.retry_attempts, + retry_backoff_seconds=config.retry_backoff_seconds, ) - return cls(client=client, cache_ttl_seconds=adapter_config.cse_cache_ttl_seconds) + return cls(client=client, cache_ttl_seconds=config.cache_ttl_seconds) async def get_prices( self, request: DeliveryCalculationRequest diff --git a/app/adapters/delivery_providers/registry.py b/app/adapters/delivery_providers/registry.py new file mode 100644 index 0000000..cd16f90 --- /dev/null +++ b/app/adapters/delivery_providers/registry.py @@ -0,0 +1,67 @@ +"""Delivery provider registry and wiring.""" + +from dataclasses import dataclass +from typing import Mapping, Sequence + +import httpx + +from app.adapters.delivery_providers.base import ( + DeliveryProvider, + OrderRegistrationProvider, + PaymentPriceValidationProvider, +) +from app.adapters.delivery_providers.cdek import CDEKProvider +from app.adapters.delivery_providers.cse import CSEProvider +from app.config import DeliveryProvidersConfig + + +@dataclass(frozen=True, slots=True) +class DeliveryProviderRegistry: + providers: Sequence[DeliveryProvider] + payment_price_validation_adapters: Mapping[str, PaymentPriceValidationProvider] + order_registration_adapters: Mapping[str, OrderRegistrationProvider] + + +def resolve_delivery_provider_timeout_seconds( + config: DeliveryProvidersConfig, +) -> float: + timeouts = [ + provider_config.timeout_seconds + for provider_config in (config.cdek, config.cse) + if provider_config.enabled + ] + return max(timeouts, default=10.0) + + +def build_delivery_provider_registry( + *, + http_client: httpx.AsyncClient, + config: DeliveryProvidersConfig, +) -> DeliveryProviderRegistry: + providers: list[DeliveryProvider] = [] + payment_price_validation_adapters: dict[str, PaymentPriceValidationProvider] = {} + order_registration_adapters: dict[str, OrderRegistrationProvider] = {} + + if config.cdek.enabled: + cdek_provider = CDEKProvider.from_config( + http_client=http_client, + config=config.cdek, + ) + providers.append(cdek_provider) + payment_price_validation_adapters[cdek_provider.name] = cdek_provider + order_registration_adapters[cdek_provider.name] = cdek_provider + + if config.cse.enabled: + cse_provider = CSEProvider.from_config( + http_client=http_client, + config=config.cse, + ) + providers.append(cse_provider) + payment_price_validation_adapters[cse_provider.name] = cse_provider + order_registration_adapters[cse_provider.name] = cse_provider + + return DeliveryProviderRegistry( + providers=tuple(providers), + payment_price_validation_adapters=payment_price_validation_adapters, + order_registration_adapters=order_registration_adapters, + ) diff --git a/app/config.py b/app/config.py index cfae51e..948175a 100644 --- a/app/config.py +++ b/app/config.py @@ -38,26 +38,36 @@ class RepositoryConfig(BaseModel): price_cache_ttl_seconds: int = 900 -class AdapterConfig(BaseModel): - cdek_base_url: str = "https://api.cdek.ru/v2" - cdek_client_id: str = "" - cdek_client_secret: str = "" - cdek_retry_attempts: int = Field(default=2, ge=0) - cdek_retry_backoff_seconds: float = Field(default=0.2, ge=0) - cdek_timeout_seconds: float = Field(default=10.0, gt=0) - cdek_cache_ttl_seconds: int = Field(default=900, gt=0) - cse_base_url: str = "https://web.cse.ru/1c/ws/Web1C.1cws" - cse_login: str = "" - cse_password: str = "" - cse_retry_attempts: int = Field(default=2, ge=0) - cse_retry_backoff_seconds: float = Field(default=0.2, ge=0) - cse_timeout_seconds: float = Field(default=10.0, gt=0) - cse_cache_ttl_seconds: int = Field(default=900, gt=0) +class DeliveryProviderBaseConfig(BaseModel): + enabled: bool = True + timeout_seconds: float = Field(default=10.0, gt=0) + retry_attempts: int = Field(default=2, ge=0) + retry_backoff_seconds: float = Field(default=0.2, ge=0) + cache_ttl_seconds: int = Field(default=900, gt=0) + + +class CDEKDeliveryProviderConfig(DeliveryProviderBaseConfig): + base_url: str = "https://api.cdek.ru/v2" + client_id: str = "" + client_secret: str = "" + + +class CSEDeliveryProviderConfig(DeliveryProviderBaseConfig): + base_url: str = "https://web.cse.ru/1c/ws/Web1C.1cws" + login: str = "" + password: str = "" # Contract-specific required parameters for SaveDocuments (order registration). # Urgency is not here: it comes from the tariff selected by the client. - cse_payer: str = "" - cse_payment_method: str = "" - cse_shipping_method: str = "" + payer: str = "" + payment_method: str = "" + shipping_method: str = "" + + +class DeliveryProvidersConfig(BaseModel): + cdek: CDEKDeliveryProviderConfig = Field( + default_factory=CDEKDeliveryProviderConfig + ) + cse: CSEDeliveryProviderConfig = Field(default_factory=CSEDeliveryProviderConfig) class TBankPaymentAuthConfig(BaseModel): @@ -157,7 +167,9 @@ class Settings(BaseSettings): service: ServiceConfig = Field(default_factory=ServiceConfig) business_logic: BusinessLogicConfig = Field(default_factory=BusinessLogicConfig) repository: RepositoryConfig = Field(default_factory=RepositoryConfig) - adapter: AdapterConfig = Field(default_factory=AdapterConfig) + delivery_providers: DeliveryProvidersConfig = Field( + default_factory=DeliveryProvidersConfig + ) tbank_payment: TBankPaymentConfig postgres: PostgresConfig address_suggestions: AddressSuggestionsConfig = Field( @@ -195,7 +207,7 @@ class _RequiredYamlSections(BaseModel): service: dict[str, Any] business_logic: dict[str, Any] repository: dict[str, Any] - adapter: dict[str, Any] + delivery_providers: dict[str, Any] tbank_payment: dict[str, Any] postgres: dict[str, Any] address_suggestions: dict[str, Any] diff --git a/app/controllers/v1/delivery.py b/app/controllers/v1/delivery.py index 62f5099..7cbd60f 100644 --- a/app/controllers/v1/delivery.py +++ b/app/controllers/v1/delivery.py @@ -10,8 +10,10 @@ from app.adapters.address_suggestions.tomtom import TomTomAddressSuggestionProvi from app.adapters.address_suggestions.yandex_geosuggest import ( YandexGeosuggestAddressSuggestionProvider, ) -from app.adapters.delivery_providers.cdek import CDEKProvider -from app.adapters.delivery_providers.cse import CSEProvider +from app.adapters.delivery_providers.registry import ( + build_delivery_provider_registry, + resolve_delivery_provider_timeout_seconds, +) from app.adapters.email import SMTPEmailSender from app.adapters.tbank import TBankAdapter from app.config import Settings @@ -49,15 +51,13 @@ _AGGREGATOR_SERVICE_STATE_KEY = "aggregator_service" def _build_aggregator_service(settings: Settings) -> AggregatorService: http_client = build_controller_http_client( - timeout_seconds=settings.adapter.cdek_timeout_seconds + timeout_seconds=resolve_delivery_provider_timeout_seconds( + settings.delivery_providers + ) ) - cdek_provider = CDEKProvider.from_adapter_config( + delivery_provider_registry = build_delivery_provider_registry( http_client=http_client, - adapter_config=settings.adapter, - ) - cse_provider = CSEProvider.from_adapter_config( - http_client=http_client, - adapter_config=settings.adapter, + config=settings.delivery_providers, ) payment_adapter = TBankAdapter.from_config( http_client=http_client, @@ -75,7 +75,6 @@ def _build_aggregator_service(settings: Settings) -> AggregatorService: http_client=http_client, config=settings.address_suggestions.tomtom, ) - providers = (cdek_provider, cse_provider) cache = PriceCache.from_repository_config( settings.repository, metrics=get_cache_metrics(), @@ -93,18 +92,16 @@ def _build_aggregator_service(settings: Settings) -> AggregatorService: timeout_seconds=settings.email.timeout_seconds, ) service = AggregatorService( - providers=providers, + providers=delivery_provider_registry.providers, cache=cache, payment_adapter=payment_adapter, - payment_price_validation_adapters={ - cdek_provider.name: cdek_provider, - cse_provider.name: cse_provider, - }, + payment_price_validation_adapters=( + delivery_provider_registry.payment_price_validation_adapters + ), order_repository=order_repository, - order_registration_adapters={ - cdek_provider.name: cdek_provider, - cse_provider.name: cse_provider, - }, + order_registration_adapters=( + delivery_provider_registry.order_registration_adapters + ), email_sender=email_sender, address_suggestion_providers=( dadata_provider, diff --git a/app/repositories/order/models.py b/app/repositories/order/models.py index 12d1232..357cdca 100644 --- a/app/repositories/order/models.py +++ b/app/repositories/order/models.py @@ -45,12 +45,13 @@ class Order(Base): ) payment_status: Mapped[str | None] = mapped_column(String(64), nullable=True) tbank_payment_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True) - cdek_order_uuid: Mapped[str | None] = mapped_column(String(128), nullable=True) - cse_order_number: Mapped[str | None] = mapped_column(String(128), nullable=True) - cdek_order_status: Mapped[str | None] = mapped_column(String(64), nullable=True) - cdek_waybill_uuid: Mapped[str | None] = mapped_column(String(128), nullable=True) - cdek_waybill_url: Mapped[str | None] = mapped_column(String(2048), nullable=True) - cdek_polled_at: Mapped[datetime | None] = mapped_column( + provider_order_id: Mapped[str | None] = mapped_column(String(128), nullable=True) + provider_order_status: Mapped[str | None] = mapped_column(String(64), nullable=True) + provider_waybill_id: Mapped[str | None] = mapped_column(String(128), nullable=True) + provider_waybill_url: Mapped[str | None] = mapped_column( + String(2048), nullable=True + ) + provider_polled_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) diff --git a/app/repositories/order/repository.py b/app/repositories/order/repository.py index f555286..c069952 100644 --- a/app/repositories/order/repository.py +++ b/app/repositories/order/repository.py @@ -75,31 +75,17 @@ class OrderRepository: await session.flush() return order - async def mark_cdek_order_registered( + async def mark_provider_order_registered( self, session: AsyncSession, order_uuid: str, - cdek_order_uuid: str, + provider_order_id: str, ) -> Order | None: order = await self.get_order_by_order_uuid(session, order_uuid) if order is None: return None - order.cdek_order_uuid = cdek_order_uuid - await session.flush() - return order - - async def mark_cse_order_registered( - self, - session: AsyncSession, - order_uuid: str, - cse_order_number: str, - ) -> Order | None: - order = await self.get_order_by_order_uuid(session, order_uuid) - if order is None: - return None - - order.cse_order_number = cse_order_number + order.provider_order_id = provider_order_id await session.flush() return order @@ -112,14 +98,15 @@ class OrderRepository: statement = ( select(Order) .where( - Order.cdek_order_uuid.is_not(None), - Order.cdek_waybill_url.is_(None), + Order.provider == "cdek", + Order.provider_order_id.is_not(None), + Order.provider_waybill_url.is_(None), ( - Order.cdek_order_status.is_(None) - | Order.cdek_order_status.not_in(TERMINAL_ORDER_STATUSES) + Order.provider_order_status.is_(None) + | Order.provider_order_status.not_in(TERMINAL_ORDER_STATUSES) ), ) - .order_by(Order.cdek_polled_at.asc().nulls_first()) + .order_by(Order.provider_polled_at.asc().nulls_first()) .limit(limit) .with_for_update(skip_locked=True) ) @@ -139,10 +126,10 @@ class OrderRepository: if order is None: return None - order.cdek_order_status = order_status - if waybill_uuid is not None and order.cdek_waybill_uuid is None: - order.cdek_waybill_uuid = waybill_uuid - order.cdek_polled_at = polled_at + order.provider_order_status = order_status + if waybill_uuid is not None and order.provider_waybill_id is None: + order.provider_waybill_id = waybill_uuid + order.provider_polled_at = polled_at await session.flush() return order @@ -158,9 +145,9 @@ class OrderRepository: if order is None: return None - if waybill_url is not None and order.cdek_waybill_url is None: - order.cdek_waybill_url = waybill_url - order.cdek_polled_at = polled_at + if waybill_url is not None and order.provider_waybill_url is None: + order.provider_waybill_url = waybill_url + order.provider_polled_at = polled_at await session.flush() return order @@ -189,7 +176,8 @@ class OrderRepository: statement = ( select(Order) .where( - Order.cdek_waybill_url.is_not(None), + Order.provider == "cdek", + Order.provider_waybill_url.is_not(None), Order.waybill_email_sent_at.is_(None), ) .order_by(Order.created_at.asc()) diff --git a/app/services/aggregator.py b/app/services/aggregator.py index a374ee5..5e2c943 100644 --- a/app/services/aggregator.py +++ b/app/services/aggregator.py @@ -141,18 +141,11 @@ class OrderRepositoryProtocol(Protocol): payment_id: int, ) -> object | None: ... - async def mark_cdek_order_registered( + async def mark_provider_order_registered( self, session: object, order_uuid: str, - cdek_order_uuid: str, - ) -> object | None: ... - - async def mark_cse_order_registered( - self, - session: object, - order_uuid: str, - cse_order_number: str, + provider_order_id: str, ) -> object | None: ... @@ -357,7 +350,7 @@ class AggregatorService: error=str(exc), ) raise InvalidInitPaymentRequestError( - "Payment init request is invalid for CDEK price validation." + "Payment init request is invalid for provider price validation." ) from exc except ProviderClientError as exc: logger.warning( @@ -389,7 +382,7 @@ class AggregatorService: requested_price_kopecks=requested_price, ) raise InvalidInitPaymentRequestError( - "CDEK did not return the requested tariff for payment validation." + "Provider did not return the requested tariff for payment validation." ) expected_amount_kopecks = calculate_expected_payment_amount_kopecks( @@ -411,7 +404,7 @@ class AggregatorService: provider_price=str(getattr(provider_price, "price", None)), ) raise InvalidInitPaymentRequestError( - "Payment amount does not match CDEK validated delivery price." + "Payment amount does not match provider validated delivery price." ) async def handle_tbank_payment_notification( @@ -593,11 +586,7 @@ class AggregatorService: @staticmethod def _has_existing_registration(order: object, provider: str) -> bool: - if provider == "cdek": - return bool(getattr(order, "cdek_order_uuid", None)) - if provider == "cse": - return bool(getattr(order, "cse_order_number", None)) - return False + return bool(getattr(order, "provider_order_id", None)) @staticmethod def _extract_registration_id(result: object) -> str: @@ -646,18 +635,11 @@ class AggregatorService: registration_id = self._extract_registration_id(result) try: async with self._order_repository.session() as session: - if provider == "cse": - order = await self._order_repository.mark_cse_order_registered( - session, - order_uuid, - registration_id, - ) - else: - order = await self._order_repository.mark_cdek_order_registered( - session, - order_uuid, - registration_id, - ) + order = await self._order_repository.mark_provider_order_registered( + session, + order_uuid, + registration_id, + ) if order is None: logger.warning( "order_registration_order_not_found", diff --git a/app/services/waybill_email_sender.py b/app/services/waybill_email_sender.py index 9fdb588..5603068 100644 --- a/app/services/waybill_email_sender.py +++ b/app/services/waybill_email_sender.py @@ -41,7 +41,7 @@ class EmailSenderProtocol(Protocol): class OrderRecord(Protocol): order_uuid: str account_email: str - cdek_waybill_url: str | None + provider_waybill_url: str | None class WaybillEmailSenderRepositoryProtocol(Protocol): @@ -133,7 +133,7 @@ class WaybillEmailSenderService: continue async def _handle_order(self, session: object, order: OrderRecord) -> None: - waybill_url = order.cdek_waybill_url + waybill_url = order.provider_waybill_url if waybill_url is None: return diff --git a/app/services/waybill_poller.py b/app/services/waybill_poller.py index ec42b8d..19d3e6c 100644 --- a/app/services/waybill_poller.py +++ b/app/services/waybill_poller.py @@ -27,8 +27,8 @@ class CDEKWaybillInfoAdapterProtocol(Protocol): class OrderRecord(Protocol): order_uuid: str - cdek_order_uuid: str | None - cdek_waybill_uuid: str | None + provider_order_id: str | None + provider_waybill_id: str | None class WaybillPollerRepositoryProtocol(Protocol): @@ -97,8 +97,8 @@ class WaybillPollerService: logger.exception( "waybill_poll_order_failed", order_uuid=order.order_uuid, - cdek_order_uuid=order.cdek_order_uuid, - cdek_waybill_uuid=order.cdek_waybill_uuid, + provider_order_id=order.provider_order_id, + provider_waybill_id=order.provider_waybill_id, ) return PollBatchSummary( processed=len(orders), @@ -133,11 +133,11 @@ class WaybillPollerService: async def _handle_order(self, session: object, order: OrderRecord) -> None: polled_at = self._datetime_now() - if order.cdek_waybill_uuid is None: - cdek_order_uuid = order.cdek_order_uuid - if cdek_order_uuid is None: + if order.provider_waybill_id is None: + provider_order_id = order.provider_order_id + if provider_order_id is None: return - info = await self._order_info_adapter.get_order(cdek_order_uuid) + info = await self._order_info_adapter.get_order(provider_order_id) await self._repository.record_order_poll( session, order_uuid=order.order_uuid, @@ -148,13 +148,15 @@ class WaybillPollerService: logger.info( "waybill_poll_order_result", order_uuid=order.order_uuid, - cdek_order_uuid=cdek_order_uuid, - cdek_order_status=info.status_code, - cdek_waybill_uuid=info.waybill_uuid, + provider_order_id=provider_order_id, + provider_order_status=info.status_code, + provider_waybill_id=info.waybill_uuid, ) return - waybill = await self._waybill_info_adapter.get_waybill(order.cdek_waybill_uuid) + waybill = await self._waybill_info_adapter.get_waybill( + order.provider_waybill_id + ) await self._repository.record_waybill_poll( session, order_uuid=order.order_uuid, @@ -164,6 +166,6 @@ class WaybillPollerService: logger.info( "waybill_poll_waybill_result", order_uuid=order.order_uuid, - cdek_waybill_uuid=order.cdek_waybill_uuid, - cdek_waybill_url=waybill.url, + provider_waybill_id=order.provider_waybill_id, + provider_waybill_url=waybill.url, ) diff --git a/app/workers/waybill_email_sender.py b/app/workers/waybill_email_sender.py index bcb65be..d1fbe7f 100644 --- a/app/workers/waybill_email_sender.py +++ b/app/workers/waybill_email_sender.py @@ -22,21 +22,22 @@ logger = structlog.get_logger(__name__) async def _run(settings: Settings, stop_event: asyncio.Event) -> None: - http_client = httpx.AsyncClient(timeout=settings.adapter.cdek_timeout_seconds) + cdek_config = settings.delivery_providers.cdek + http_client = httpx.AsyncClient(timeout=cdek_config.timeout_seconds) auth_client = CDEKAuthClient( http_client=http_client, - base_url=settings.adapter.cdek_base_url, - client_id=settings.adapter.cdek_client_id, - client_secret=settings.adapter.cdek_client_secret, - timeout_seconds=settings.adapter.cdek_timeout_seconds, + base_url=cdek_config.base_url, + client_id=cdek_config.client_id, + client_secret=cdek_config.client_secret, + timeout_seconds=cdek_config.timeout_seconds, ) cdek_client = CDEKClient( http_client=http_client, auth_client=auth_client, - base_url=settings.adapter.cdek_base_url, - timeout_seconds=settings.adapter.cdek_timeout_seconds, - retry_attempts=settings.adapter.cdek_retry_attempts, - retry_backoff_seconds=settings.adapter.cdek_retry_backoff_seconds, + base_url=cdek_config.base_url, + timeout_seconds=cdek_config.timeout_seconds, + retry_attempts=cdek_config.retry_attempts, + retry_backoff_seconds=cdek_config.retry_backoff_seconds, ) email_sender = SMTPEmailSender( smtp_host=settings.email.smtp_host, diff --git a/app/workers/waybill_poller.py b/app/workers/waybill_poller.py index 5ef65d5..189c66a 100644 --- a/app/workers/waybill_poller.py +++ b/app/workers/waybill_poller.py @@ -21,21 +21,22 @@ logger = structlog.get_logger(__name__) async def _run(settings: Settings, stop_event: asyncio.Event) -> None: - http_client = httpx.AsyncClient(timeout=settings.adapter.cdek_timeout_seconds) + cdek_config = settings.delivery_providers.cdek + http_client = httpx.AsyncClient(timeout=cdek_config.timeout_seconds) auth_client = CDEKAuthClient( http_client=http_client, - base_url=settings.adapter.cdek_base_url, - client_id=settings.adapter.cdek_client_id, - client_secret=settings.adapter.cdek_client_secret, - timeout_seconds=settings.adapter.cdek_timeout_seconds, + base_url=cdek_config.base_url, + client_id=cdek_config.client_id, + client_secret=cdek_config.client_secret, + timeout_seconds=cdek_config.timeout_seconds, ) cdek_client = CDEKClient( http_client=http_client, auth_client=auth_client, - base_url=settings.adapter.cdek_base_url, - timeout_seconds=settings.adapter.cdek_timeout_seconds, - retry_attempts=settings.adapter.cdek_retry_attempts, - retry_backoff_seconds=settings.adapter.cdek_retry_backoff_seconds, + base_url=cdek_config.base_url, + timeout_seconds=cdek_config.timeout_seconds, + retry_attempts=cdek_config.retry_attempts, + retry_backoff_seconds=cdek_config.retry_backoff_seconds, ) engine = create_postgres_engine(settings.postgres) session_factory = create_postgres_session_factory(engine) diff --git a/config.template.yaml b/config.template.yaml index 3f647cc..65661f8 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -14,24 +14,28 @@ repository: redis_dsn: "redis://redis:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.edu.cdek.ru/v2" - cdek_client_id: "${CDEK_CLIENT_ID}" - cdek_client_secret: "${CDEK_CLIENT_SECRET}" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 - cse_base_url: "http://lk-test.cse.ru/1c/ws/web1c.1cws" - cse_login: "${CSE_LOGIN}" - cse_password: "${CSE_PASSWORD}" - cse_retry_attempts: 2 - cse_retry_backoff_seconds: 0.2 - cse_timeout_seconds: 10.0 - cse_cache_ttl_seconds: 900 - cse_payer: "0" # Заказчик - cse_payment_method: "1" # Безналичный расчёт - cse_shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd" # авто +delivery_providers: + cdek: + enabled: true + base_url: "https://api.edu.cdek.ru/v2" + client_id: "${CDEK_CLIENT_ID}" + client_secret: "${CDEK_CLIENT_SECRET}" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: true + base_url: "http://lk-test.cse.ru/1c/ws/web1c.1cws" + login: "${CSE_LOGIN}" + password: "${CSE_PASSWORD}" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + payer: "0" # Заказчик + payment_method: "1" # Безналичный расчёт + shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd" # авто tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/config.test.yaml b/config.test.yaml index fb38b51..50f686b 100644 --- a/config.test.yaml +++ b/config.test.yaml @@ -14,24 +14,28 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 - cse_base_url: "http://lk-test.cse.ru/1c/ws/web1c.1cws" - cse_login: "test" - cse_password: "2016" - cse_retry_attempts: 2 - cse_retry_backoff_seconds: 0.2 - cse_timeout_seconds: 10.0 - cse_cache_ttl_seconds: 900 - cse_payer: "0" - cse_payment_method: "1" - cse_shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd" +delivery_providers: + cdek: + enabled: true + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: true + base_url: "http://lk-test.cse.ru/1c/ws/web1c.1cws" + login: "test" + password: "2016" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + payer: "0" + payment_method: "1" + shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd" tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/adapters/delivery_providers/cdek/test_client.py b/tests/adapters/delivery_providers/cdek/test_client.py index e27ef40..09576b2 100644 --- a/tests/adapters/delivery_providers/cdek/test_client.py +++ b/tests/adapters/delivery_providers/cdek/test_client.py @@ -13,7 +13,7 @@ from app.adapters.delivery_providers.cdek.client import ( ) from app.cities import cities_map from app import config as config_module -from app.config import AdapterConfig, Settings +from app.config import CDEKDeliveryProviderConfig, Settings from app.schemas.request import DeliveryCalculationRequest, DeliveryEntity @@ -287,14 +287,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.test/v2" - cdek_client_id: "yaml-id" - cdek_client_secret: "yaml-secret" - cdek_retry_attempts: 0 - cdek_retry_backoff_seconds: 0.1 - cdek_timeout_seconds: 7.5 - cdek_cache_ttl_seconds: 777 +delivery_providers: + cdek: + base_url: "https://api.cdek.test/v2" + client_id: "yaml-id" + client_secret: "yaml-secret" + retry_attempts: 0 + retry_backoff_seconds: 0.1 + timeout_seconds: 7.5 + cache_ttl_seconds: 777 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" @@ -323,9 +326,9 @@ email: monkeypatch.setattr(config_module, "_resolve_runtime_config_file", lambda: str(config_file)) settings = Settings() http_client = RecordingHTTPClient() - provider = CDEKProvider.from_adapter_config( + provider = CDEKProvider.from_config( http_client=http_client, # type: ignore[arg-type] - adapter_config=settings.adapter, + config=settings.delivery_providers.cdek, ) result = asyncio.run(provider.get_prices(_make_request())) @@ -349,14 +352,14 @@ email: def test_provider_uses_default_adapter_timeout_and_cache_ttl() -> None: - adapter_config = AdapterConfig( - cdek_client_id="default-id", - cdek_client_secret="default-secret", + config = CDEKDeliveryProviderConfig( + client_id="default-id", + client_secret="default-secret", ) http_client = RecordingHTTPClient() - provider = CDEKProvider.from_adapter_config( + provider = CDEKProvider.from_config( http_client=http_client, # type: ignore[arg-type] - adapter_config=adapter_config, + config=config, ) result = asyncio.run(provider.get_prices(_make_request())) diff --git a/tests/adapters/delivery_providers/test_registry.py b/tests/adapters/delivery_providers/test_registry.py new file mode 100644 index 0000000..7ea9aaa --- /dev/null +++ b/tests/adapters/delivery_providers/test_registry.py @@ -0,0 +1,50 @@ +import asyncio + +import httpx + +from app.adapters.delivery_providers.registry import ( + build_delivery_provider_registry, + resolve_delivery_provider_timeout_seconds, +) +from app.config import ( + CDEKDeliveryProviderConfig, + CSEDeliveryProviderConfig, + DeliveryProvidersConfig, +) + + +def test_registry_builds_enabled_provider_capability_maps() -> None: + config = DeliveryProvidersConfig( + cdek=CDEKDeliveryProviderConfig( + base_url="https://cdek.test/v2", + client_id="id", + client_secret="secret", + cache_ttl_seconds=111, + ), + cse=CSEDeliveryProviderConfig( + enabled=False, + ), + ) + + http_client = httpx.AsyncClient() + try: + registry = build_delivery_provider_registry( + http_client=http_client, + config=config, + ) + finally: + asyncio.run(http_client.aclose()) + + assert [provider.name for provider in registry.providers] == ["cdek"] + assert sorted(registry.payment_price_validation_adapters) == ["cdek"] + assert sorted(registry.order_registration_adapters) == ["cdek"] + assert registry.providers[0].cache_ttl_seconds == 111 + + +def test_resolve_delivery_provider_timeout_uses_max_enabled_timeout() -> None: + config = DeliveryProvidersConfig( + cdek=CDEKDeliveryProviderConfig(timeout_seconds=3.0), + cse=CSEDeliveryProviderConfig(timeout_seconds=7.5), + ) + + assert resolve_delivery_provider_timeout_seconds(config) == 7.5 diff --git a/tests/config/fixtures/config.default.yaml b/tests/config/fixtures/config.default.yaml index 3abbe33..72d7221 100644 --- a/tests/config/fixtures/config.default.yaml +++ b/tests/config/fixtures/config.default.yaml @@ -11,9 +11,12 @@ business_logic: repository: redis_dsn: "redis://localhost:6379/0" -adapter: - cdek_client_id: "yaml-id" - cdek_client_secret: "yaml-secret" +delivery_providers: + cdek: + client_id: "yaml-id" + client_secret: "yaml-secret" + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.invalid_price_multiplier.yaml b/tests/config/fixtures/config.invalid_price_multiplier.yaml index 8082562..e822f2e 100644 --- a/tests/config/fixtures/config.invalid_price_multiplier.yaml +++ b/tests/config/fixtures/config.invalid_price_multiplier.yaml @@ -14,14 +14,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.missing_address_suggestions.yaml b/tests/config/fixtures/config.missing_address_suggestions.yaml index dd809d6..23bf38d 100644 --- a/tests/config/fixtures/config.missing_address_suggestions.yaml +++ b/tests/config/fixtures/config.missing_address_suggestions.yaml @@ -14,14 +14,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.missing_observability.yaml b/tests/config/fixtures/config.missing_observability.yaml index 2160432..1e8fb31 100644 --- a/tests/config/fixtures/config.missing_observability.yaml +++ b/tests/config/fixtures/config.missing_observability.yaml @@ -14,14 +14,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.missing_observability_endpoint.yaml b/tests/config/fixtures/config.missing_observability_endpoint.yaml index 89ef903..0fc6557 100644 --- a/tests/config/fixtures/config.missing_observability_endpoint.yaml +++ b/tests/config/fixtures/config.missing_observability_endpoint.yaml @@ -14,14 +14,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.missing_price_multiplier.yaml b/tests/config/fixtures/config.missing_price_multiplier.yaml index 3ac901e..8bb82e4 100644 --- a/tests/config/fixtures/config.missing_price_multiplier.yaml +++ b/tests/config/fixtures/config.missing_price_multiplier.yaml @@ -13,14 +13,17 @@ repository: redis_dsn: "redis://localhost:6379/0" price_cache_ttl_seconds: 900 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/fixtures/config.test.override.yaml b/tests/config/fixtures/config.test.override.yaml index a7b7682..83e5760 100644 --- a/tests/config/fixtures/config.test.override.yaml +++ b/tests/config/fixtures/config.test.override.yaml @@ -11,9 +11,12 @@ business_logic: repository: redis_dsn: "redis://localhost:6379/0" -adapter: - cdek_client_id: "test-id" - cdek_client_secret: "test-secret" +delivery_providers: + cdek: + client_id: "test-id" + client_secret: "test-secret" + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/config/test_config_sections.py b/tests/config/test_config_sections.py index 348f341..583fd94 100644 --- a/tests/config/test_config_sections.py +++ b/tests/config/test_config_sections.py @@ -132,7 +132,7 @@ business_logic: repository: {{}} -adapter: {{}} +delivery_providers: {{}} tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" @@ -174,13 +174,31 @@ def test_configuration_sections_are_loaded_from_yaml_file( assert settings.business_logic.provider_price_multiplier == Decimal("1.0") assert settings.repository.redis_dsn == "redis://localhost:6379/0" assert settings.repository.price_cache_ttl_seconds == 900 - assert settings.adapter.cdek_base_url == "https://api.cdek.ru/v2" - assert settings.adapter.cdek_client_id == "test-client-id" - assert settings.adapter.cdek_client_secret == "test-client-secret" - assert settings.adapter.cdek_retry_attempts == 2 - assert settings.adapter.cdek_retry_backoff_seconds == 0.2 - assert settings.adapter.cdek_timeout_seconds == 10.0 - assert settings.adapter.cdek_cache_ttl_seconds == 900 + assert settings.delivery_providers.cdek.enabled is True + assert settings.delivery_providers.cdek.base_url == "https://api.cdek.ru/v2" + assert settings.delivery_providers.cdek.client_id == "test-client-id" + assert settings.delivery_providers.cdek.client_secret == "test-client-secret" + assert settings.delivery_providers.cdek.retry_attempts == 2 + assert settings.delivery_providers.cdek.retry_backoff_seconds == 0.2 + assert settings.delivery_providers.cdek.timeout_seconds == 10.0 + assert settings.delivery_providers.cdek.cache_ttl_seconds == 900 + assert settings.delivery_providers.cse.enabled is True + assert ( + settings.delivery_providers.cse.base_url + == "http://lk-test.cse.ru/1c/ws/web1c.1cws" + ) + assert settings.delivery_providers.cse.login == "test" + assert settings.delivery_providers.cse.password == "2016" + assert settings.delivery_providers.cse.retry_attempts == 2 + assert settings.delivery_providers.cse.retry_backoff_seconds == 0.2 + assert settings.delivery_providers.cse.timeout_seconds == 10.0 + assert settings.delivery_providers.cse.cache_ttl_seconds == 900 + assert settings.delivery_providers.cse.payer == "0" + assert settings.delivery_providers.cse.payment_method == "1" + assert ( + settings.delivery_providers.cse.shipping_method + == "5052d0b3-5ea3-46f2-823f-1472686a51dd" + ) assert settings.tbank_payment.init_url == "https://securepay.tinkoff.ru/v2/Init" assert ( settings.tbank_payment.notification_url @@ -233,7 +251,7 @@ def test_get_settings_fails_when_required_yaml_section_is_missing( get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} - assert ("adapter",) in locations + assert ("delivery_providers",) in locations get_settings.cache_clear() @@ -287,7 +305,7 @@ def test_get_settings_uses_config_test_yaml_in_pytest_environment( assert settings.service.provider_timeout_seconds == 17.0 assert settings.controller.api_prefix == "/from-config-test-yaml" assert settings.business_logic.provider_price_multiplier == Decimal("1.25") - assert settings.adapter.cdek_client_id == "test-id" + assert settings.delivery_providers.cdek.client_id == "test-id" assert settings.tbank_payment.auth.terminal_key == "override-terminal-key" assert settings.tbank_payment.auth.password == "override-password" assert ( diff --git a/tests/controllers/v1/test_address_suggestions.py b/tests/controllers/v1/test_address_suggestions.py index df5459e..531e96b 100644 --- a/tests/controllers/v1/test_address_suggestions.py +++ b/tests/controllers/v1/test_address_suggestions.py @@ -5,6 +5,7 @@ import pytest from app.controllers.v1 import delivery as delivery_controller from app.controllers.v1.delivery import get_aggregator_service +from app.adapters.delivery_providers.registry import DeliveryProviderRegistry from app.main import create_app from app.schemas.request import AddressSuggestRequest, SuggestAddressRequest from app.schemas.response import AddressSuggestion @@ -52,16 +53,6 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency( async def aclose(self) -> None: return None - class StubCDEKProvider: - name = "stub-cdek" - cache_ttl_seconds = 900 - - @classmethod - def from_adapter_config(cls, *, http_client, adapter_config) -> "StubCDEKProvider": - assert http_client is stub_http_client - _ = adapter_config - return cls() - class StubAddressProvider: def __init__(self, name: str, response: list[AddressSuggestion]) -> None: self.name = name @@ -169,12 +160,38 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency( http_client_timeouts.append(timeout_seconds) return stub_http_client + def fake_resolve_delivery_provider_timeout_seconds(config: object) -> float: + _ = config + return 10.0 + + def fake_build_delivery_provider_registry( + *, + http_client: object, + config: object, + ) -> DeliveryProviderRegistry: + assert http_client is stub_http_client + _ = config + return DeliveryProviderRegistry( + providers=(), + payment_price_validation_adapters={}, + order_registration_adapters={}, + ) + monkeypatch.setattr( delivery_controller, "build_controller_http_client", fake_build_controller_http_client, ) - monkeypatch.setattr(delivery_controller, "CDEKProvider", StubCDEKProvider) + monkeypatch.setattr( + delivery_controller, + "resolve_delivery_provider_timeout_seconds", + fake_resolve_delivery_provider_timeout_seconds, + ) + monkeypatch.setattr( + delivery_controller, + "build_delivery_provider_registry", + fake_build_delivery_provider_registry, + ) monkeypatch.setattr( delivery_controller, "DadataAddressSuggestionProvider", diff --git a/tests/controllers/v1/test_delivery.py b/tests/controllers/v1/test_delivery.py index 8f8324e..cd374c6 100644 --- a/tests/controllers/v1/test_delivery.py +++ b/tests/controllers/v1/test_delivery.py @@ -9,6 +9,7 @@ from app.controllers.v1.delivery import get_aggregator_service from app.main import create_app from app.schemas.request import DeliveryCalculationRequest, DeliveryEntity, ParcelType from app.schemas.response import DeliveryPrice +from app.adapters.delivery_providers.registry import DeliveryProviderRegistry from app.services.aggregator import ( AggregatorService, AggregatorServiceError, @@ -118,13 +119,6 @@ def test_post_delivery_price_uses_registered_provider_in_default_dependency( ), ] - class StubCDEKProvider: - @classmethod - def from_adapter_config(cls, *, http_client, adapter_config) -> StubProvider: - assert http_client is stub_http_client - _ = adapter_config - return stub_provider - class StubCache: def __init__(self) -> None: self.storage: dict[str, object] = {} @@ -154,12 +148,38 @@ def test_post_delivery_price_uses_registered_provider_in_default_dependency( http_client_timeouts.append(timeout_seconds) return stub_http_client + def fake_resolve_delivery_provider_timeout_seconds(config: object) -> float: + _ = config + return 12.5 + + def fake_build_delivery_provider_registry( + *, + http_client: object, + config: object, + ) -> DeliveryProviderRegistry: + assert http_client is stub_http_client + _ = config + return DeliveryProviderRegistry( + providers=(stub_provider,), + payment_price_validation_adapters={}, + order_registration_adapters={}, + ) + monkeypatch.setattr( delivery_controller, "build_controller_http_client", fake_build_controller_http_client, ) - monkeypatch.setattr(delivery_controller, "CDEKProvider", StubCDEKProvider) + monkeypatch.setattr( + delivery_controller, + "resolve_delivery_provider_timeout_seconds", + fake_resolve_delivery_provider_timeout_seconds, + ) + monkeypatch.setattr( + delivery_controller, + "build_delivery_provider_registry", + fake_build_delivery_provider_registry, + ) monkeypatch.setattr(delivery_controller, "PriceCache", StubPriceCache) app = create_app() @@ -199,7 +219,7 @@ def test_post_delivery_price_uses_registered_provider_in_default_dependency( } ] assert second_response.json() == first_response.json() - assert len(http_client_timeouts) == 1 + assert http_client_timeouts == [12.5] assert len(stub_provider.calls) == 1 diff --git a/tests/repositories/cache/test_redis_cache.py b/tests/repositories/cache/test_redis_cache.py index 7b46b8e..21475f5 100644 --- a/tests/repositories/cache/test_redis_cache.py +++ b/tests/repositories/cache/test_redis_cache.py @@ -186,14 +186,17 @@ repository: redis_dsn: "redis://redis.internal:6380/5" price_cache_ttl_seconds: 123 -adapter: - cdek_base_url: "https://api.cdek.ru/v2" - cdek_client_id: "test-client-id" - cdek_client_secret: "test-client-secret" - cdek_retry_attempts: 2 - cdek_retry_backoff_seconds: 0.2 - cdek_timeout_seconds: 10.0 - cdek_cache_ttl_seconds: 900 +delivery_providers: + cdek: + base_url: "https://api.cdek.ru/v2" + client_id: "test-client-id" + client_secret: "test-client-secret" + retry_attempts: 2 + retry_backoff_seconds: 0.2 + timeout_seconds: 10.0 + cache_ttl_seconds: 900 + cse: + enabled: false tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" diff --git a/tests/repositories/order/test_repository.py b/tests/repositories/order/test_repository.py index bee5a0b..4a42f3f 100644 --- a/tests/repositories/order/test_repository.py +++ b/tests/repositories/order/test_repository.py @@ -69,9 +69,9 @@ def test_create_order_persists_all_required_fields() -> None: assert persisted_order.payload == order_data.payload assert persisted_order.payment_status is None assert persisted_order.tbank_payment_id is None - assert persisted_order.cdek_order_uuid is None - assert persisted_order.cdek_waybill_uuid is None - assert persisted_order.cdek_waybill_url is None + assert persisted_order.provider_order_id is None + assert persisted_order.provider_waybill_id is None + assert persisted_order.provider_waybill_url is None assert persisted_order.created_at is not None assert persisted_order.updated_at is not None @@ -175,7 +175,7 @@ def test_mark_payment_status_returns_none_for_missing_order() -> None: asyncio.run(_with_repository(run)) -def test_mark_cdek_order_registered_persists_cdek_order_uuid_only() -> None: +def test_mark_provider_order_registered_persists_provider_order_id_only() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], @@ -184,7 +184,7 @@ def test_mark_cdek_order_registered_persists_cdek_order_uuid_only() -> None: await repository.create_order(session, _make_order_data()) async with repository.session() as session: - order = await repository.mark_cdek_order_registered( + order = await repository.mark_provider_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", @@ -197,14 +197,14 @@ def test_mark_cdek_order_registered_persists_cdek_order_uuid_only() -> None: persisted_order = result.scalar_one() assert order is not None - assert persisted_order.cdek_order_uuid == "cdek-order-uuid-1" - assert persisted_order.cdek_waybill_uuid is None - assert persisted_order.cdek_waybill_url is None + assert persisted_order.provider_order_id == "cdek-order-uuid-1" + assert persisted_order.provider_waybill_id is None + assert persisted_order.provider_waybill_url is None asyncio.run(_with_repository(run)) -def test_mark_cdek_order_registered_is_idempotent_for_same_uuid() -> None: +def test_mark_provider_order_registered_is_idempotent_for_same_uuid() -> None: async def run( repository: OrderRepository, session_factory: async_sessionmaker[AsyncSession], @@ -213,14 +213,14 @@ def test_mark_cdek_order_registered_is_idempotent_for_same_uuid() -> None: await repository.create_order(session, _make_order_data()) async with repository.session() as session: - await repository.mark_cdek_order_registered( + await repository.mark_provider_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", ) async with repository.session() as session: - await repository.mark_cdek_order_registered( + await repository.mark_provider_order_registered( session, "order-uuid-1", "cdek-order-uuid-1", @@ -231,18 +231,18 @@ def test_mark_cdek_order_registered_is_idempotent_for_same_uuid() -> None: orders = result.scalars().all() assert len(orders) == 1 - assert orders[0].cdek_order_uuid == "cdek-order-uuid-1" + assert orders[0].provider_order_id == "cdek-order-uuid-1" asyncio.run(_with_repository(run)) -def test_mark_cdek_order_registered_returns_none_for_missing_order() -> None: +def test_mark_provider_order_registered_returns_none_for_missing_order() -> None: async def run( repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: async with repository.session() as session: - order = await repository.mark_cdek_order_registered( + order = await repository.mark_provider_order_registered( session, "missing-order", "cdek-order-uuid-1", @@ -257,37 +257,38 @@ async def _seed_order( repository: OrderRepository, *, order_uuid: str, - cdek_order_uuid: str | None, - cdek_order_status: str | None = None, - cdek_waybill_uuid: str | None = None, - cdek_waybill_url: str | None = None, - cdek_polled_at: datetime | None = None, + provider_order_id: str | None, + provider: str = "cdek", + provider_order_status: str | None = None, + provider_waybill_id: str | None = None, + provider_waybill_url: str | None = None, + provider_polled_at: datetime | None = None, ) -> None: async with repository.session() as session: await repository.create_order( - session, _make_order_data(order_uuid=order_uuid) + session, _make_order_data(order_uuid=order_uuid, provider=provider) ) - if cdek_order_uuid is not None: - await repository.mark_cdek_order_registered( - session, order_uuid, cdek_order_uuid + if provider_order_id is not None: + await repository.mark_provider_order_registered( + session, order_uuid, provider_order_id ) if ( - cdek_order_status is not None - or cdek_waybill_uuid is not None - or cdek_polled_at is not None + provider_order_status is not None + or provider_waybill_id is not None + or provider_polled_at is not None ): order = await repository.get_order_by_order_uuid(session, order_uuid) assert order is not None - if cdek_order_status is not None: - order.cdek_order_status = cdek_order_status - if cdek_waybill_uuid is not None: - order.cdek_waybill_uuid = cdek_waybill_uuid - if cdek_polled_at is not None: - order.cdek_polled_at = cdek_polled_at - if cdek_waybill_url is not None: + if provider_order_status is not None: + order.provider_order_status = provider_order_status + if provider_waybill_id is not None: + order.provider_waybill_id = provider_waybill_id + if provider_polled_at is not None: + order.provider_polled_at = provider_polled_at + if provider_waybill_url is not None: order = await repository.get_order_by_order_uuid(session, order_uuid) assert order is not None - order.cdek_waybill_url = cdek_waybill_url + order.provider_waybill_url = provider_waybill_url def test_list_orders_pending_waybill_returns_orders_without_url() -> None: @@ -295,21 +296,26 @@ def test_list_orders_pending_waybill_returns_orders_without_url() -> None: repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: - await _seed_order(repository, order_uuid="pending", cdek_order_uuid="o1") + await _seed_order(repository, order_uuid="pending", provider_order_id="o1") await _seed_order( repository, order_uuid="done", - cdek_order_uuid="o2", - cdek_waybill_uuid="w2", - cdek_waybill_url="https://cdek.test/2.pdf", + provider_order_id="o2", + provider_waybill_id="w2", + provider_waybill_url="https://cdek.test/2.pdf", ) await _seed_order( repository, order_uuid="invalid", - cdek_order_uuid="o3", - cdek_order_status="INVALID", + provider_order_id="o3", + provider_order_status="INVALID", + ) + await _seed_order( + repository, + order_uuid="not-cdek", + provider="cse", + provider_order_id="cse-1", ) - await _seed_order(repository, order_uuid="no-cdek", cdek_order_uuid=None) async with repository.session() as session: orders = await repository.list_orders_pending_waybill(session, limit=10) @@ -330,16 +336,16 @@ def test_list_orders_pending_waybill_orders_polled_at_nulls_first() -> None: await _seed_order( repository, order_uuid="late", - cdek_order_uuid="o-late", - cdek_polled_at=later, + provider_order_id="o-late", + provider_polled_at=later, ) await _seed_order( repository, order_uuid="early", - cdek_order_uuid="o-early", - cdek_polled_at=earlier, + provider_order_id="o-early", + provider_polled_at=earlier, ) - await _seed_order(repository, order_uuid="never", cdek_order_uuid="o-never") + await _seed_order(repository, order_uuid="never", provider_order_id="o-never") async with repository.session() as session: orders = await repository.list_orders_pending_waybill(session, limit=10) @@ -356,7 +362,7 @@ def test_record_order_poll_sets_status_and_waybill_uuid() -> None: repository: OrderRepository, _session_factory: async_sessionmaker[AsyncSession], ) -> None: - await _seed_order(repository, order_uuid="o", cdek_order_uuid="cdek-o") + await _seed_order(repository, order_uuid="o", provider_order_id="cdek-o") async with repository.session() as session: order = await repository.record_order_poll( @@ -368,9 +374,9 @@ def test_record_order_poll_sets_status_and_waybill_uuid() -> None: ) assert order is not None - assert order.cdek_order_status == "ACCEPTED" - assert order.cdek_waybill_uuid == "waybill-1" - assert order.cdek_polled_at == polled + assert order.provider_order_status == "ACCEPTED" + assert order.provider_waybill_id == "waybill-1" + assert order.provider_polled_at == polled asyncio.run(_with_repository(run)) @@ -385,8 +391,8 @@ def test_record_order_poll_does_not_overwrite_existing_waybill_uuid() -> None: await _seed_order( repository, order_uuid="o", - cdek_order_uuid="cdek-o", - cdek_waybill_uuid="existing-waybill", + provider_order_id="cdek-o", + provider_waybill_id="existing-waybill", ) async with repository.session() as session: @@ -399,7 +405,7 @@ def test_record_order_poll_does_not_overwrite_existing_waybill_uuid() -> None: ) assert order is not None - assert order.cdek_waybill_uuid == "existing-waybill" + assert order.provider_waybill_id == "existing-waybill" asyncio.run(_with_repository(run)) @@ -414,8 +420,8 @@ def test_record_waybill_poll_sets_url_only_when_previously_null() -> None: await _seed_order( repository, order_uuid="o", - cdek_order_uuid="cdek-o", - cdek_waybill_uuid="waybill-1", + provider_order_id="cdek-o", + provider_waybill_id="waybill-1", ) async with repository.session() as session: @@ -427,8 +433,8 @@ def test_record_waybill_poll_sets_url_only_when_previously_null() -> None: ) assert order is not None - assert order.cdek_waybill_url == "https://cdek.test/1.pdf" - assert order.cdek_polled_at == polled + assert order.provider_waybill_url == "https://cdek.test/1.pdf" + assert order.provider_polled_at == polled async with repository.session() as session: order = await repository.record_waybill_poll( @@ -438,7 +444,7 @@ def test_record_waybill_poll_sets_url_only_when_previously_null() -> None: polled_at=polled, ) assert order is not None - assert order.cdek_waybill_url == "https://cdek.test/1.pdf" + assert order.provider_waybill_url == "https://cdek.test/1.pdf" asyncio.run(_with_repository(run)) @@ -451,22 +457,22 @@ def test_list_orders_pending_waybill_email_returns_orders_with_url_and_no_sent_a await _seed_order( repository, order_uuid="ready", - cdek_order_uuid="o1", - cdek_waybill_uuid="w1", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_order_id="o1", + provider_waybill_id="w1", + provider_waybill_url="https://cdek.test/1.pdf", ) await _seed_order( repository, order_uuid="no-url", - cdek_order_uuid="o2", - cdek_waybill_uuid="w2", + provider_order_id="o2", + provider_waybill_id="w2", ) await _seed_order( repository, order_uuid="already-sent", - cdek_order_uuid="o3", - cdek_waybill_uuid="w3", - cdek_waybill_url="https://cdek.test/3.pdf", + provider_order_id="o3", + provider_waybill_id="w3", + provider_waybill_url="https://cdek.test/3.pdf", ) async with repository.session() as session: sent = await repository.get_order_by_order_uuid(session, "already-sent") @@ -493,16 +499,16 @@ def test_list_orders_pending_waybill_email_orders_by_created_at_asc() -> None: await _seed_order( repository, order_uuid="first", - cdek_order_uuid="o1", - cdek_waybill_uuid="w1", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_order_id="o1", + provider_waybill_id="w1", + provider_waybill_url="https://cdek.test/1.pdf", ) await _seed_order( repository, order_uuid="second", - cdek_order_uuid="o2", - cdek_waybill_uuid="w2", - cdek_waybill_url="https://cdek.test/2.pdf", + provider_order_id="o2", + provider_waybill_id="w2", + provider_waybill_url="https://cdek.test/2.pdf", ) async with repository.session() as session: @@ -526,9 +532,9 @@ def test_record_waybill_email_sent_sets_timestamp_once() -> None: await _seed_order( repository, order_uuid="o", - cdek_order_uuid="cdek-o", - cdek_waybill_uuid="w", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_order_id="cdek-o", + provider_waybill_id="w", + provider_waybill_url="https://cdek.test/1.pdf", ) async with repository.session() as session: @@ -580,8 +586,8 @@ def test_record_waybill_poll_updates_polled_at_when_url_is_none() -> None: await _seed_order( repository, order_uuid="o", - cdek_order_uuid="cdek-o", - cdek_waybill_uuid="waybill-1", + provider_order_id="cdek-o", + provider_waybill_id="waybill-1", ) async with repository.session() as session: @@ -593,7 +599,7 @@ def test_record_waybill_poll_updates_polled_at_when_url_is_none() -> None: ) assert order is not None - assert order.cdek_waybill_url is None - assert order.cdek_polled_at == polled + assert order.provider_waybill_url is None + assert order.provider_polled_at == polled asyncio.run(_with_repository(run)) diff --git a/tests/services/test_provider_routing.py b/tests/services/test_provider_routing.py index 7a185cf..b9cf9a7 100644 --- a/tests/services/test_provider_routing.py +++ b/tests/services/test_provider_routing.py @@ -115,8 +115,7 @@ class StoredOrder: ) payment_status: str | None = None tbank_payment_id: int | None = None - cdek_order_uuid: str | None = None - cse_order_number: str | None = None + provider_order_id: str | None = None payment_email_sent_at: object | None = None account_email: str | None = None @@ -148,17 +147,12 @@ class StubOrderRepository: self._order.tbank_payment_id = payment_id return self._order - async def mark_cse_order_registered( - self, session: object, order_uuid: str, cse_order_number: str + async def mark_provider_order_registered( + self, session: object, order_uuid: str, provider_order_id: str ) -> StoredOrder: - self._order.cse_order_number = cse_order_number + self._order.provider_order_id = provider_order_id return self._order - async def mark_cdek_order_registered( - self, session: object, order_uuid: str, cdek_order_uuid: str - ) -> StoredOrder: - raise AssertionError("CDEK persistence must not be used for a CSE order.") - def _notification() -> TBankPaymentNotification: return TBankPaymentNotification( @@ -190,4 +184,4 @@ def test_notification_routes_registration_to_order_provider() -> None: assert result == "OK" assert len(cse_registration.calls) == 1 - assert order.cse_order_number == "CSE-000123" + assert order.provider_order_id == "CSE-000123" diff --git a/tests/services/test_tbank_notifications.py b/tests/services/test_tbank_notifications.py index 389ebea..6a29722 100644 --- a/tests/services/test_tbank_notifications.py +++ b/tests/services/test_tbank_notifications.py @@ -32,10 +32,9 @@ class StoredOrder: payload: dict[str, Any] = field(default_factory=_default_payload) payment_status: str | None = None tbank_payment_id: int | None = None - cdek_order_uuid: str | None = None - cse_order_number: str | None = None - cdek_waybill_uuid: str | None = None - cdek_waybill_url: str | None = None + provider_order_id: str | None = None + provider_waybill_id: str | None = None + provider_waybill_url: str | None = None payment_email_sent_at: object | None = None @@ -72,10 +71,10 @@ class StubOrderRepository: self, *, orders: list[StoredOrder] | None = None, - mark_cdek_errors: list[Exception | None] | None = None, + mark_provider_errors: list[Exception | None] | None = None, ) -> None: self._orders = {order.order_uuid: order for order in orders or []} - self._mark_cdek_errors = mark_cdek_errors or [] + self._mark_provider_errors = mark_provider_errors or [] self.session_value = object() self.calls: list[tuple[str, tuple[object, ...]]] = [] @@ -113,24 +112,24 @@ class StubOrderRepository: order.tbank_payment_id = payment_id return order - async def mark_cdek_order_registered( + async def mark_provider_order_registered( self, session: object, order_uuid: str, - cdek_order_uuid: str, + provider_order_id: str, ) -> StoredOrder | None: self.calls.append( - ("mark_cdek_order_registered", (session, order_uuid, cdek_order_uuid)) + ("mark_provider_order_registered", (session, order_uuid, provider_order_id)) ) - if self._mark_cdek_errors: - error = self._mark_cdek_errors.pop(0) + if self._mark_provider_errors: + error = self._mark_provider_errors.pop(0) if error is not None: raise error order = self._orders.get(order_uuid) if order is None: return None - order.cdek_order_uuid = cdek_order_uuid + order.provider_order_id = provider_order_id return order async def record_payment_email_sent( @@ -230,15 +229,15 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None: assert payment_adapter.notifications == [notification] assert order.payment_status == "CONFIRMED" assert order.tbank_payment_id == 8347568144 - assert order.cdek_order_uuid == "cdek-order-uuid-1" - assert order.cdek_waybill_uuid is None - assert order.cdek_waybill_url is None + assert order.provider_order_id == "cdek-order-uuid-1" + assert order.provider_waybill_id is None + assert order.provider_waybill_url is None assert len(cdek_adapter.calls) == 1 assert cdek_adapter.calls[0][1] == "order-uuid-1" def test_duplicate_confirmed_notification_does_not_call_cdek() -> None: - order = StoredOrder(cdek_order_uuid="existing-cdek-order-uuid") + order = StoredOrder(provider_order_id="existing-cdek-order-uuid") cdek_adapter = StubCDEKOrderAdapter() service = AggregatorService( providers=[], @@ -253,7 +252,7 @@ def test_duplicate_confirmed_notification_does_not_call_cdek() -> None: assert result == "OK" assert cdek_adapter.calls == [] - assert order.cdek_order_uuid == "existing-cdek-order-uuid" + assert order.provider_order_id == "existing-cdek-order-uuid" def test_non_confirmed_notification_acknowledges_without_cdek() -> None: @@ -330,7 +329,7 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id() order = StoredOrder() order_repository = StubOrderRepository( orders=[order], - mark_cdek_errors=[RuntimeError("db down"), None], + mark_provider_errors=[RuntimeError("db down"), None], ) cdek_adapter = StubCDEKOrderAdapter( responses=[ @@ -357,12 +356,12 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id() with pytest.raises(TBankPaymentNotificationProcessingError): asyncio.run(service.handle_tbank_payment_notification(notification)) - assert order.cdek_order_uuid is None + assert order.provider_order_id is None result = asyncio.run(service.handle_tbank_payment_notification(notification)) assert result == "OK" - assert order.cdek_order_uuid == "same-cdek-order-uuid" + assert order.provider_order_id == "same-cdek-order-uuid" assert [order_uuid for _, order_uuid in cdek_adapter.calls] == [ "order-uuid-1", "order-uuid-1", @@ -417,7 +416,7 @@ def test_duplicate_notification_does_not_resend_payment_email() -> None: from datetime import datetime, timezone order = StoredOrder( - cdek_order_uuid="existing-cdek-order-uuid", + provider_order_id="existing-cdek-order-uuid", payment_email_sent_at=datetime(2026, 1, 1, tzinfo=timezone.utc), ) email_sender = StubEmailSender() @@ -454,4 +453,4 @@ def test_email_failure_does_not_break_notification_handling() -> None: assert result == "OK" assert len(email_sender.calls) == 1 - assert order.cdek_order_uuid == "cdek-order-uuid-1" + assert order.provider_order_id == "cdek-order-uuid-1" diff --git a/tests/services/test_waybill_email_sender.py b/tests/services/test_waybill_email_sender.py index 311e647..b8e4448 100644 --- a/tests/services/test_waybill_email_sender.py +++ b/tests/services/test_waybill_email_sender.py @@ -10,7 +10,7 @@ from app.services.waybill_email_sender import WaybillEmailSenderService class StoredOrder: order_uuid: str account_email: str - cdek_waybill_url: str | None = None + provider_waybill_url: str | None = None waybill_email_sent_at: datetime | None = None @@ -41,7 +41,7 @@ class StubRepository: return [ order for order in self._orders.values() - if order.cdek_waybill_url is not None + if order.provider_waybill_url is not None and order.waybill_email_sent_at is None ] @@ -125,7 +125,7 @@ def test_poll_once_downloads_pdf_sends_email_and_marks_sent() -> None: order = StoredOrder( order_uuid="o-1", account_email="client@example.com", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_waybill_url="https://cdek.test/1.pdf", ) repo = StubRepository([order]) downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"}) @@ -155,7 +155,7 @@ def test_poll_once_download_error_keeps_order_pending_and_skips_send() -> None: order = StoredOrder( order_uuid="o-1", account_email="client@example.com", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_waybill_url="https://cdek.test/1.pdf", ) repo = StubRepository([order]) downloader = StubDownloader({"https://cdek.test/1.pdf": RuntimeError("cdek 500")}) @@ -177,7 +177,7 @@ def test_poll_once_smtp_error_keeps_order_pending() -> None: order = StoredOrder( order_uuid="o-1", account_email="bad@example.com", - cdek_waybill_url="https://cdek.test/1.pdf", + provider_waybill_url="https://cdek.test/1.pdf", ) repo = StubRepository([order]) downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"}) @@ -197,12 +197,12 @@ def test_poll_once_failure_on_one_order_does_not_break_batch() -> None: bad = StoredOrder( order_uuid="bad", account_email="bad@example.com", - cdek_waybill_url="https://cdek.test/bad.pdf", + provider_waybill_url="https://cdek.test/bad.pdf", ) good = StoredOrder( order_uuid="good", account_email="good@example.com", - cdek_waybill_url="https://cdek.test/good.pdf", + provider_waybill_url="https://cdek.test/good.pdf", ) repo = StubRepository([bad, good]) downloader = StubDownloader( diff --git a/tests/services/test_waybill_poller.py b/tests/services/test_waybill_poller.py index 14fdcc8..26c3e32 100644 --- a/tests/services/test_waybill_poller.py +++ b/tests/services/test_waybill_poller.py @@ -13,11 +13,11 @@ from app.services.waybill_poller import WaybillPollerService @dataclass class StoredOrder: order_uuid: str - cdek_order_uuid: str | None = None - cdek_order_status: str | None = None - cdek_waybill_uuid: str | None = None - cdek_waybill_url: str | None = None - cdek_polled_at: datetime | None = None + provider_order_id: str | None = None + provider_order_status: str | None = None + provider_waybill_id: str | None = None + provider_waybill_url: str | None = None + provider_polled_at: datetime | None = None class StubSessionContext: @@ -69,10 +69,10 @@ class StubRepository: order = self._orders.get(order_uuid) if order is None: return None - order.cdek_order_status = order_status - if waybill_uuid is not None and order.cdek_waybill_uuid is None: - order.cdek_waybill_uuid = waybill_uuid - order.cdek_polled_at = polled_at + order.provider_order_status = order_status + if waybill_uuid is not None and order.provider_waybill_id is None: + order.provider_waybill_id = waybill_uuid + order.provider_polled_at = polled_at return order async def record_waybill_poll( @@ -96,9 +96,9 @@ class StubRepository: order = self._orders.get(order_uuid) if order is None: return None - if waybill_url is not None and order.cdek_waybill_url is None: - order.cdek_waybill_url = waybill_url - order.cdek_polled_at = polled_at + if waybill_url is not None and order.provider_waybill_url is None: + order.provider_waybill_url = waybill_url + order.provider_polled_at = polled_at return order @@ -107,9 +107,9 @@ class StubOrderInfoAdapter: self._results = results self.calls: list[str] = [] - async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo: - self.calls.append(cdek_order_uuid) - result = self._results[cdek_order_uuid] + async def get_order(self, provider_order_id: str) -> CDEKOrderInfo: + self.calls.append(provider_order_id) + result = self._results[provider_order_id] if isinstance(result, Exception): raise result return result @@ -120,9 +120,9 @@ class StubWaybillInfoAdapter: self._results = results self.calls: list[str] = [] - async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo: - self.calls.append(cdek_waybill_uuid) - result = self._results[cdek_waybill_uuid] + async def get_waybill(self, provider_waybill_id: str) -> CDEKWaybillInfo: + self.calls.append(provider_waybill_id) + result = self._results[provider_waybill_id] if isinstance(result, Exception): raise result return result @@ -147,7 +147,7 @@ def _make_service( def test_poll_once_fetches_order_info_when_waybill_uuid_is_missing() -> None: - order = StoredOrder(order_uuid="o", cdek_order_uuid="cdek-o") + order = StoredOrder(order_uuid="o", provider_order_id="cdek-o") repo = StubRepository([order]) order_info = StubOrderInfoAdapter( { @@ -164,16 +164,16 @@ def test_poll_once_fetches_order_info_when_waybill_uuid_is_missing() -> None: assert summary.processed == 1 and summary.succeeded == 1 and summary.failed == 0 assert order_info.calls == ["cdek-o"] - assert order.cdek_order_status == "ACCEPTED" - assert order.cdek_waybill_uuid == "waybill-1" - assert order.cdek_polled_at == _POLLED_AT + assert order.provider_order_status == "ACCEPTED" + assert order.provider_waybill_id == "waybill-1" + assert order.provider_polled_at == _POLLED_AT def test_poll_once_fetches_waybill_info_when_waybill_uuid_is_present() -> None: order = StoredOrder( order_uuid="o", - cdek_order_uuid="cdek-o", - cdek_waybill_uuid="waybill-1", + provider_order_id="cdek-o", + provider_waybill_id="waybill-1", ) repo = StubRepository([order]) waybill_info = StubWaybillInfoAdapter( @@ -190,12 +190,12 @@ def test_poll_once_fetches_waybill_info_when_waybill_uuid_is_present() -> None: assert summary.processed == 1 and summary.succeeded == 1 and summary.failed == 0 assert waybill_info.calls == ["waybill-1"] - assert order.cdek_waybill_url == "https://cdek.test/1.pdf" - assert order.cdek_polled_at == _POLLED_AT + assert order.provider_waybill_url == "https://cdek.test/1.pdf" + assert order.provider_polled_at == _POLLED_AT def test_poll_once_records_terminal_status_without_waybill() -> None: - order = StoredOrder(order_uuid="o", cdek_order_uuid="cdek-o") + order = StoredOrder(order_uuid="o", provider_order_id="cdek-o") repo = StubRepository([order]) order_info = StubOrderInfoAdapter( { @@ -211,16 +211,16 @@ def test_poll_once_records_terminal_status_without_waybill() -> None: summary = asyncio.run(service.poll_once()) assert summary.succeeded == 1 - assert order.cdek_order_status == "INVALID" - assert order.cdek_waybill_uuid is None + assert order.provider_order_status == "INVALID" + assert order.provider_waybill_id is None def test_poll_once_failure_on_one_order_does_not_break_batch() -> None: - failing = StoredOrder(order_uuid="bad", cdek_order_uuid="cdek-bad") + failing = StoredOrder(order_uuid="bad", provider_order_id="cdek-bad") good = StoredOrder( order_uuid="good", - cdek_order_uuid="cdek-good", - cdek_waybill_uuid="waybill-good", + provider_order_id="cdek-good", + provider_waybill_id="waybill-good", ) repo = StubRepository([failing, good]) order_info = StubOrderInfoAdapter({"cdek-bad": RuntimeError("cdek down")}) @@ -241,7 +241,7 @@ def test_poll_once_failure_on_one_order_does_not_break_batch() -> None: assert summary.processed == 2 assert summary.succeeded == 1 assert summary.failed == 1 - assert good.cdek_waybill_url == "https://cdek.test/good.pdf" + assert good.provider_waybill_url == "https://cdek.test/good.pdf" def test_run_forever_exits_when_stop_event_is_set() -> None: diff --git a/tests/workers/test_waybill_email_sender_main.py b/tests/workers/test_waybill_email_sender_main.py index aa4c9c3..bdca3d3 100644 --- a/tests/workers/test_waybill_email_sender_main.py +++ b/tests/workers/test_waybill_email_sender_main.py @@ -3,7 +3,8 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from app.config import ( - AdapterConfig, + CDEKDeliveryProviderConfig, + DeliveryProvidersConfig, EmailAdapterConfig, ObservabilityConfig, PostgresConfig, @@ -17,10 +18,12 @@ from app.workers.waybill_email_sender import _run def _make_settings() -> Settings: return Settings( - adapter=AdapterConfig( - cdek_base_url="https://api.cdek.test/v2", - cdek_client_id="id", - cdek_client_secret="secret", + delivery_providers=DeliveryProvidersConfig( + cdek=CDEKDeliveryProviderConfig( + base_url="https://api.cdek.test/v2", + client_id="id", + client_secret="secret", + ) ), tbank_payment=TBankPaymentConfig( init_url="https://pay.test/init", diff --git a/tests/workers/test_waybill_poller_main.py b/tests/workers/test_waybill_poller_main.py index e1c5073..af28ce3 100644 --- a/tests/workers/test_waybill_poller_main.py +++ b/tests/workers/test_waybill_poller_main.py @@ -10,7 +10,8 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from app.config import ( - AdapterConfig, + CDEKDeliveryProviderConfig, + DeliveryProvidersConfig, EmailAdapterConfig, ObservabilityConfig, PostgresConfig, @@ -24,10 +25,12 @@ from app.workers.waybill_poller import _run def _make_settings() -> Settings: return Settings( - adapter=AdapterConfig( - cdek_base_url="https://api.cdek.test/v2", - cdek_client_id="id", - cdek_client_secret="secret", + delivery_providers=DeliveryProvidersConfig( + cdek=CDEKDeliveryProviderConfig( + base_url="https://api.cdek.test/v2", + client_id="id", + client_secret="secret", + ) ), tbank_payment=TBankPaymentConfig( init_url="https://pay.test/init",