import asyncio import pytest from app.adapters.tbank.base import ( TBankPaymentAdapterError, TBankPaymentRequestError, ) from app.repositories.order import OrderData from app.schemas.payment import InitPaymentRequest, InitPaymentResponse from app.services.aggregator import ( AggregatorService, InitPaymentUnavailableError, InvalidInitPaymentRequestError, ) class StubPaymentAdapter: def __init__( self, *, response: str | None = None, error: Exception | None = None, ) -> None: self._response = response self._error = error self.calls: list[tuple[str, int]] = [] async def create_payment_link(self, order_uuid: str, amount_kopecks: int) -> str: self.calls.append((order_uuid, amount_kopecks)) if self._error is not None: raise self._error if self._response is None: raise RuntimeError("Stub payment adapter has no response configured.") return self._response class StubOrderSessionContext: def __init__(self, session: object) -> None: self._session = session async def __aenter__(self) -> object: return self._session async def __aexit__(self, exc_type, exc, traceback) -> None: return None class StubOrderRepository: def __init__(self, *, error: Exception | None = None) -> None: self._error = error self.session_value = object() self.calls: list[tuple[object, OrderData]] = [] def session(self) -> StubOrderSessionContext: return StubOrderSessionContext(self.session_value) async def create_order(self, session: object, order_data: OrderData) -> object: self.calls.append((session, order_data)) if self._error is not None: raise self._error return object() def _make_init_payment_request(**overrides: object) -> InitPaymentRequest: payload: dict[str, object] = { "order_uuid": "order-uuid-1", "price": 125000, "type": 2, "tariff_code": 535, "comment": "Test payment", "sender": { "name": "Petr Petrov", "email": "sender@example.com", "phone": {"number": "+79009876543"}, }, "recipient": { "name": "Ivan Ivanov", "email": "ivan@example.com", "phone": {"number": "+79001234567"}, }, "from_location": { "address": "Lenina 1", "city": "Moscow", "country_code": "RU", }, "to_location": { "address": "Pushkina 10", "city": "Novosibirsk", "country_code": "RU", }, "services": [{"code": "INSURANCE", "parameter": "1000"}], "packages": [ { "number": "1", "weight": 1, "length": 20, "width": 15, "height": 10, "comment": "Package 1", } ], } payload.update(overrides) return InitPaymentRequest(**payload) def test_init_payment_delegates_to_adapter_and_returns_payment_url() -> None: request = _make_init_payment_request() adapter = StubPaymentAdapter(response="https://pay.test/payment/1") service = AggregatorService(providers=[], payment_adapter=adapter) result = asyncio.run(service.init_payment(request)) assert result == InitPaymentResponse(payment_url="https://pay.test/payment/1") assert adapter.calls == [("order-uuid-1", 125000)] def test_init_payment_persists_order_after_successful_payment_link() -> None: request = _make_init_payment_request() adapter = StubPaymentAdapter(response="https://pay.test/payment/1") order_repository = StubOrderRepository() service = AggregatorService( providers=[], payment_adapter=adapter, order_repository=order_repository, ) result = asyncio.run(service.init_payment(request)) assert result == InitPaymentResponse(payment_url="https://pay.test/payment/1") assert adapter.calls == [("order-uuid-1", 125000)] assert order_repository.calls == [ ( order_repository.session_value, OrderData( order_uuid="order-uuid-1", payment_url="https://pay.test/payment/1", price=125000, delivery_type=2, tariff_code=535, sender={ "name": "Petr Petrov", "email": "sender@example.com", "phone": {"number": "+79009876543"}, }, recipient={ "name": "Ivan Ivanov", "email": "ivan@example.com", "phone": {"number": "+79001234567"}, }, from_location={ "address": "Lenina 1", "city": "Moscow", "country_code": "RU", }, to_location={ "address": "Pushkina 10", "city": "Novosibirsk", "country_code": "RU", }, packages=[ { "number": "1", "weight": 1, "length": 20, "width": 15, "height": 10, "comment": "Package 1", } ], services=[{"code": "INSURANCE", "parameter": "1000"}], comment="Test payment", ), ) ] def test_init_payment_returns_payment_url_when_order_persistence_fails() -> None: request = _make_init_payment_request() adapter = StubPaymentAdapter(response="https://pay.test/payment/1") order_repository = StubOrderRepository(error=RuntimeError("database down")) service = AggregatorService( providers=[], payment_adapter=adapter, order_repository=order_repository, ) result = asyncio.run(service.init_payment(request)) assert result == InitPaymentResponse(payment_url="https://pay.test/payment/1") assert adapter.calls == [("order-uuid-1", 125000)] assert len(order_repository.calls) == 1 def test_init_payment_maps_provider_request_errors_to_invalid_payment_error() -> None: request = _make_init_payment_request() adapter = StubPaymentAdapter(error=TBankPaymentRequestError("bad payload")) service = AggregatorService(providers=[], payment_adapter=adapter) with pytest.raises(InvalidInitPaymentRequestError): asyncio.run(service.init_payment(request)) assert adapter.calls == [("order-uuid-1", 125000)] def test_init_payment_delegates_request_with_services_none_without_new_logic() -> None: request = _make_init_payment_request(services=None) adapter = StubPaymentAdapter(response="https://pay.test/payment/2") service = AggregatorService(providers=[], payment_adapter=adapter) result = asyncio.run(service.init_payment(request)) assert result == InitPaymentResponse(payment_url="https://pay.test/payment/2") assert adapter.calls == [("order-uuid-1", 125000)] assert request.services is None def test_init_payment_maps_client_failures_to_unavailable_error() -> None: request = _make_init_payment_request() adapter = StubPaymentAdapter(error=TBankPaymentAdapterError("transport down")) service = AggregatorService(providers=[], payment_adapter=adapter) with pytest.raises(InitPaymentUnavailableError): asyncio.run(service.init_payment(request)) assert adapter.calls == [("order-uuid-1", 125000)] def test_init_payment_without_configured_adapter_raises_unavailable_error() -> None: service = AggregatorService(providers=[]) with pytest.raises(InitPaymentUnavailableError): asyncio.run(service.init_payment(_make_init_payment_request()))