Добавлен провайдер доставки CSE: SOAP-адаптер (Calc + SaveDocuments), маршрутизация init-payment по провайдеру, обобщение tariff_code до строки, география CSE в cities_map
Deploy / deploy (push) Failing after 52s

This commit is contained in:
Раис Юсупалиев
2026-05-31 20:13:52 +03:00
parent 6a2bf05ba5
commit 6c0f97adf6
41 changed files with 16113 additions and 4806 deletions
+10 -10
View File
@@ -119,7 +119,7 @@ def test_init_payment_validates_cdek_price_before_tbank_and_returns_payment_url(
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
order_uuid_factory=lambda: "order-uuid-1",
)
@@ -140,7 +140,7 @@ def test_init_payment_persists_order_payload_after_successful_payment_link() ->
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
order_repository=order_repository,
order_uuid_factory=lambda: "order-uuid-1",
)
@@ -153,7 +153,7 @@ def test_init_payment_persists_order_payload_after_successful_payment_link() ->
assert order_data.order_uuid == "order-uuid-1"
assert order_data.payment_url == "https://pay.test/payment/1"
assert order_data.price == 125000
assert order_data.tariff_code == 535
assert order_data.tariff_code == "535"
assert order_data.account_email == "client@example.com"
assert order_data.payload == request.model_dump(mode="json", by_alias=True)
@@ -166,7 +166,7 @@ def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
order_repository=order_repository,
order_uuid_factory=lambda: "order-uuid-1",
)
@@ -185,7 +185,7 @@ def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() ->
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
)
with pytest.raises(InvalidInitPaymentRequestError):
@@ -199,7 +199,7 @@ def test_init_payment_maps_client_failures_to_unavailable_error() -> None:
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
)
with pytest.raises(InitPaymentUnavailableError):
@@ -223,7 +223,7 @@ def test_init_payment_rejects_cdek_price_mismatch_without_tbank_or_repository_ca
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
order_repository=order_repository,
)
@@ -241,7 +241,7 @@ def test_init_payment_rejects_when_cdek_does_not_return_requested_tariff() -> No
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
)
with pytest.raises(InvalidInitPaymentRequestError):
@@ -259,7 +259,7 @@ def test_init_payment_maps_cdek_request_error_to_invalid_payment_error() -> None
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
)
with pytest.raises(InvalidInitPaymentRequestError):
@@ -275,7 +275,7 @@ def test_init_payment_maps_cdek_client_error_to_unavailable_error() -> None:
service = AggregatorService(
providers=[],
payment_adapter=adapter,
payment_price_validation_adapter=validation_adapter,
payment_price_validation_adapters={"cdek": validation_adapter},
)
with pytest.raises(InitPaymentUnavailableError):
+191
View File
@@ -0,0 +1,191 @@
import asyncio
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any
from app.schemas.payment import InitPaymentRequest, TBankPaymentNotification
from app.schemas.response import DeliveryPrice
from app.services.aggregator import AggregatorService
from tests.payment_fixtures import make_init_payment_payload, make_init_payment_request
class StubPaymentAdapter:
async def create_payment_link(self, order_uuid: str, amount_kopecks: int) -> str:
return "https://pay.test/payment/1"
def verify_payment_notification(self, notification: TBankPaymentNotification) -> None:
return None
class StubValidationAdapter:
def __init__(self, response: DeliveryPrice | None) -> None:
self._response = response
self.calls: list[InitPaymentRequest] = []
async def get_payment_price(
self, request: InitPaymentRequest
) -> DeliveryPrice | None:
self.calls.append(request)
return self._response
@dataclass
class CSEResult:
order_number: str = "CSE-000123"
class StubCSERegistrationAdapter:
def __init__(self) -> None:
self.calls: list[tuple[InitPaymentRequest, str]] = []
async def register_order(
self, request: InitPaymentRequest, order_uuid: str
) -> CSEResult:
self.calls.append((request, order_uuid))
return CSEResult()
class StubCDEKRegistrationAdapter:
def __init__(self) -> None:
self.calls: list[tuple[InitPaymentRequest, str]] = []
async def register_order(
self, request: InitPaymentRequest, order_uuid: str
) -> object:
self.calls.append((request, order_uuid))
raise AssertionError("CDEK adapter must not be called for a CSE order.")
def _cse_price() -> DeliveryPrice:
return DeliveryPrice.model_construct(
provider="cse",
service_name="CSE tariff",
price=Decimal("1250.00"),
currency="RUB",
delivery_days_min=1,
delivery_days_max=2,
tariff_code="cse-guid",
)
def _cse_request() -> InitPaymentRequest:
return make_init_payment_request(
systemData={
"tariff": {
"provider": "cse",
"serviceName": "CSE tariff",
"price": 125000,
"deliveryDaysMin": 1,
"deliveryDaysMax": 2,
"tariffCode": "cse-guid",
},
"parcelType": "parcel",
"weight": "1.0",
"dimensions": {"length": "10", "width": "10", "height": "10"},
}
)
def test_init_payment_routes_validation_to_tariff_provider() -> None:
cse_validation = StubValidationAdapter(_cse_price())
cdek_validation = StubValidationAdapter(None)
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
payment_price_validation_adapters={
"cdek": cdek_validation,
"cse": cse_validation,
},
provider_price_multiplier=Decimal("1"),
order_uuid_factory=lambda: "order-uuid-1",
)
asyncio.run(service.init_payment(_cse_request()))
assert len(cse_validation.calls) == 1
assert cdek_validation.calls == []
@dataclass
class StoredOrder:
order_uuid: str = "order-uuid-1"
provider: str = "cse"
payload: dict[str, Any] = field(
default_factory=lambda: _cse_request().model_dump(mode="json", by_alias=True)
)
payment_status: str | None = None
tbank_payment_id: int | None = None
cdek_order_uuid: str | None = None
cse_order_number: str | None = None
payment_email_sent_at: object | None = None
account_email: str | None = None
class StubSession:
async def __aenter__(self) -> object:
return object()
async def __aexit__(self, *args: object) -> None:
return None
class StubOrderRepository:
def __init__(self, order: StoredOrder) -> None:
self._order = order
def session(self) -> StubSession:
return StubSession()
async def get_order_by_order_uuid(self, session: object, order_uuid: str) -> StoredOrder:
return self._order
async def mark_payment_status(
self, session: object, order_uuid: str, status: str, payment_id: int
) -> StoredOrder:
self._order.payment_status = status
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
) -> StoredOrder:
self._order.cse_order_number = cse_order_number
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(
TerminalKey="TestTerminal",
OrderId="order-uuid-1",
Success=True,
Status="CONFIRMED",
PaymentId=8347568144,
ErrorCode="0",
Amount=125000,
Token="signed-token",
)
def test_notification_routes_registration_to_order_provider() -> None:
order = StoredOrder()
cse_registration = StubCSERegistrationAdapter()
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(order),
order_registration_adapters={
"cdek": StubCDEKRegistrationAdapter(),
"cse": cse_registration,
},
)
result = asyncio.run(service.handle_tbank_payment_notification(_notification()))
assert result == "OK"
assert len(cse_registration.calls) == 1
assert order.cse_order_number == "CSE-000123"
+14 -12
View File
@@ -26,12 +26,14 @@ class StoredOrder:
order_uuid: str = "order-uuid-1"
payment_url: str = "https://pay.test/payment/1"
price: int = 125000
tariff_code: int = 535
tariff_code: str = "535"
provider: str = "cdek"
account_email: str = "client@example.com"
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
payment_email_sent_at: object | None = None
@@ -216,7 +218,7 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
providers=[],
payment_adapter=payment_adapter,
order_repository=order_repository,
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
notification = _make_notification()
@@ -240,7 +242,7 @@ def test_duplicate_confirmed_notification_does_not_call_cdek() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
result = asyncio.run(
@@ -259,7 +261,7 @@ def test_non_confirmed_notification_acknowledges_without_cdek() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
result = asyncio.run(
@@ -282,7 +284,7 @@ def test_invalid_token_does_not_access_repository_or_cdek() -> None:
verify_error=TBankPaymentNotificationTokenError("bad token")
),
order_repository=order_repository,
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
with pytest.raises(InvalidTBankPaymentNotificationError):
@@ -298,7 +300,7 @@ def test_missing_order_returns_processing_error_without_cdek() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(),
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
with pytest.raises(TBankPaymentNotificationProcessingError):
@@ -313,7 +315,7 @@ def test_cdek_registration_failure_returns_processing_error() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[StoredOrder()]),
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
with pytest.raises(TBankPaymentNotificationProcessingError):
@@ -346,7 +348,7 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=order_repository,
order_registration_adapter=cdek_adapter,
order_registration_adapters={"cdek": cdek_adapter},
)
notification = _make_notification()
@@ -372,7 +374,7 @@ def test_confirmed_notification_sends_payment_confirmation_email() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
email_sender=email_sender,
)
@@ -394,7 +396,7 @@ def test_non_confirmed_notification_does_not_send_email() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
email_sender=email_sender,
)
@@ -421,7 +423,7 @@ def test_duplicate_notification_does_not_resend_payment_email() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
email_sender=email_sender,
)
@@ -440,7 +442,7 @@ def test_email_failure_does_not_break_notification_handling() -> None:
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
order_registration_adapters={"cdek": StubCDEKOrderAdapter()},
email_sender=email_sender,
)