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, *, for_update: bool = False ) -> 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"