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, ) from tests.payment_fixtures import make_init_payment_payload 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 _post(app, payload: dict[str, object]) -> httpx.Response: 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/order", json=payload) return asyncio.run(run_request()) 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) payload = make_init_payment_payload() response = _post(app, payload) assert response.status_code == 200 assert response.json() == expected_response.model_dump(mode="json") assert service.calls == [InitPaymentRequest.model_validate(payload)] def test_post_init_payment_rejects_invalid_tariff_code() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["systemData"]["tariff"]["tariffCode"] = "" response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_snake_case_top_level_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["account_email"] = invalid_payload.pop("accountEmail") response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_invalid_account_email() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["accountEmail"] = "not-an-email" response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_snake_case_nested_address_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() sender_address = invalid_payload["senderAddress"] sender_address["city_id"] = sender_address.pop("cityId") response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_snake_case_nested_tariff_field() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() tariff = invalid_payload["systemData"]["tariff"] tariff["tariff_code"] = tariff.pop("tariffCode") response = _post(app, invalid_payload) 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 = make_init_payment_payload() del invalid_payload["systemData"]["tariff"]["price"] response = _post(app, invalid_payload) 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 = make_init_payment_payload() invalid_payload["systemData"]["tariff"]["price"] = 0 response = _post(app, invalid_payload) 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 = make_init_payment_payload() invalid_payload["systemData"]["tariff"]["price"] = 125000.5 response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_missing_declared_value() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() del invalid_payload["content"]["declared_value"] response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_non_integer_declared_value() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["content"]["declared_value"] = "50000" response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_camel_case_declared_value() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["content"]["declaredValue"] = invalid_payload["content"].pop( "declared_value" ) response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_requires_company_fields_when_is_company_true() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["senderContact"]["isCompany"] = True response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_accepts_company_with_full_requisites() -> 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 = make_init_payment_payload() payload["senderContact"].update( { "isCompany": True, "companyName": "Romashka LLC", "inn": "7707083893", "kpp": "770701001", } ) response = _post(app, payload) assert response.status_code == 200 assert service.calls == [InitPaymentRequest.model_validate(payload)] def test_post_init_payment_rejects_non_numeric_weight() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["systemData"]["weight"] = "string" response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_non_numeric_dimension() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["systemData"]["dimensions"]["length"] = "string" response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_rejects_parcel_without_dimensions() -> None: service = StubAggregatorService(response=None) app = create_app() _install_service_override(app, service) invalid_payload = make_init_payment_payload() invalid_payload["systemData"]["dimensions"] = None response = _post(app, invalid_payload) assert response.status_code == 422 assert service.calls == [] def test_post_init_payment_accepts_doc_without_dimensions() -> 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 = make_init_payment_payload() payload["systemData"]["parcelType"] = "doc" payload["systemData"]["docPackaging"] = "envelope" payload["systemData"]["dimensions"] = None response = _post(app, payload) assert response.status_code == 200 assert service.calls == [InitPaymentRequest.model_validate(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) response = _post(app, make_init_payment_payload()) 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) response = _post(app, make_init_payment_payload()) assert response.status_code == 503 assert response.json() == { "detail": { "code": "init_payment_unavailable", "message": "Payment initialization is temporarily unavailable.", } }