@@ -110,6 +110,13 @@ jobs:
|
||||
render docker-compose.template.yml docker-compose.rendered.yml
|
||||
render otel-collector-config.template.yaml otel-collector-config.rendered.yaml
|
||||
|
||||
IMG=gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
docker pull "$IMG"
|
||||
docker run --rm \
|
||||
-v "$PWD/config.rendered.yaml:/config.yaml:ro" \
|
||||
"$IMG" \
|
||||
uv run --no-sync python -c "from app.config import get_settings; get_settings(); print('config ok')"
|
||||
|
||||
scp -i ~/.ssh/deploy_key config.rendered.yaml \
|
||||
deploy@194.58.121.203:$DEPLOY_DIR/config.yaml
|
||||
scp -i ~/.ssh/deploy_key docker-compose.rendered.yml \
|
||||
|
||||
@@ -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")
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
+33
-21
@@ -30,7 +30,7 @@ class ServiceConfig(BaseModel):
|
||||
|
||||
class BusinessLogicConfig(BaseModel):
|
||||
weight_round_scale: int = 2
|
||||
provider_price_multiplier: Decimal = Field(..., gt=0)
|
||||
provider_price_multiplier: Decimal = Field(default=Decimal("1"), gt=0)
|
||||
|
||||
|
||||
class RepositoryConfig(BaseModel):
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
http_client=http_client,
|
||||
adapter_config=settings.adapter,
|
||||
)
|
||||
cse_provider = CSEProvider.from_adapter_config(
|
||||
delivery_provider_registry = build_delivery_provider_registry(
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,14 +635,7 @@ 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(
|
||||
order = await self._order_repository.mark_provider_order_registered(
|
||||
session,
|
||||
order_uuid,
|
||||
registration_id,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+22
-18
@@ -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"
|
||||
|
||||
+22
-18
@@ -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"
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
+11
-8
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user