import asyncio import httpx from app.controllers.v1.delivery import get_aggregator_service from app.main import create_app from app.schemas.payment import InitPaymentRequest, InitPaymentResponse from app.services.aggregator import ( InitPaymentUnavailableError, InvalidInitPaymentRequestError, ) class StubAggregatorService: def __init__(self, *, response: object, error: Exception | None = None) -> None: self._response = response self._error = error self.calls: list[InitPaymentRequest] = [] async def init_payment(self, request: InitPaymentRequest) -> object: self.calls.append(request) if self._error is not None: raise self._error return self._response def _install_service_override(app, service: StubAggregatorService) -> None: async def override_service() -> StubAggregatorService: return service app.dependency_overrides[get_aggregator_service] = override_service def _valid_payload() -> dict[str, object]: return { "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", } ], } def test_post_init_payment_returns_response_and_delegates_to_service() -> None: expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1") service = StubAggregatorService(response=expected_response) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 200 assert response.json() == expected_response.model_dump(mode="json") assert service.calls == [InitPaymentRequest(**_valid_payload())] def test_post_init_payment_rejects_invalid_payload() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["tariff_code"] = 136 async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_missing_price() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() del invalid_payload["price"] async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_non_positive_price() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["price"] = 0 async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_non_integer_price() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["price"] = 125000.5 async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_sender_company_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["sender"] = { **invalid_payload["sender"], # type: ignore[arg-type] "company": "Romashka LLC", } async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_sender_phones_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["sender"] = { **invalid_payload["sender"], # type: ignore[arg-type] "phones": [{"number": "+79009876543"}], } async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_recipient_company_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["recipient"] = { **invalid_payload["recipient"], # type: ignore[arg-type] "company": "Romashka LLC", } async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_recipient_phones_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = _valid_payload() invalid_payload["recipient"] = { **invalid_payload["recipient"], # type: ignore[arg-type] "phones": [{"number": "+79001234567"}], } async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=invalid_payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_accepts_request_without_services() -> None: expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1") service = StubAggregatorService(response=expected_response) app = create_app() _install_service_override(app, service) payload = _valid_payload() del payload["services"] async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post("/api/v1/delivery/init-payment", json=payload) response = asyncio.run(run_request()) assert response.status_code == 200 assert response.json() == expected_response.model_dump(mode="json") assert service.calls == [InitPaymentRequest(**payload)] def test_post_init_payment_maps_invalid_request_to_400() -> None: service = StubAggregatorService( response=None, error=InvalidInitPaymentRequestError("invalid payload"), ) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 400 assert response.json() == { "detail": { "code": "invalid_init_payment_request", "message": "Payment request contains invalid or unsupported TBank data.", } } def test_post_init_payment_maps_service_exception_to_503() -> None: service = StubAggregatorService( response=None, error=InitPaymentUnavailableError("service unavailable"), ) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/init-payment", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 503 assert response.json() == { "detail": { "code": "init_payment_unavailable", "message": "Payment initialization is temporarily unavailable.", } } def test_delivery_order_endpoint_is_removed() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post("/api/v1/delivery/order", json=_valid_payload()) response = asyncio.run(run_request()) assert response.status_code == 404 assert service.calls == []