Рефактор
Deploy / deploy (push) Failing after 14m35s

This commit is contained in:
Раис Юсупалиев
2026-06-26 19:29:11 +03:00
parent c6c37640fd
commit 3f7c6dc631
37 changed files with 802 additions and 461 deletions
@@ -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
+6 -3
View File
@@ -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"
+28 -10
View File
@@ -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",
+29 -9
View File
@@ -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
View File
@@ -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"
+85 -79
View File
@@ -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))
+5 -11
View File
@@ -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"
+21 -22
View File
@@ -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"
+7 -7
View File
@@ -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(
+33 -33
View File
@@ -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",
+8 -5
View File
@@ -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",