From e229b625f6ff2d755dd8af5d4fbdd7073a9da0d2 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 16 Oct 2025 16:19:24 +0300 Subject: [PATCH] Retry Pal24 bill creation with multiple method identifiers --- app/services/payment/pal24.py | 75 ++++++++++--- tests/services/test_payment_service_pal24.py | 110 ++++++++++++++++++- 2 files changed, 170 insertions(+), 15 deletions(-) diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 2283aca2..7ec2fcad 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -64,22 +64,42 @@ class Pal24PaymentMixin: } normalized_payment_method = self._normalize_payment_method(payment_method) - payment_module = import_module("app.services.payment_service") - try: - response = await service.create_bill( - amount_kopeks=amount_kopeks, - user_id=user_id, - order_id=order_id, - description=description, - ttl_seconds=ttl_seconds, - custom_payload=custom_payload, - payer_email=payer_email, - payment_method=normalized_payment_method, - ) - except Pal24APIError as error: - logger.error("Ошибка Pal24 API при создании счета: %s", error) + response: Optional[Dict[str, Any]] = None + pal24_payment_method: Optional[str] = None + last_error: Optional[Exception] = None + + for candidate in self._get_payment_method_candidates(normalized_payment_method): + try: + response = await service.create_bill( + amount_kopeks=amount_kopeks, + user_id=user_id, + order_id=order_id, + description=description, + ttl_seconds=ttl_seconds, + custom_payload=custom_payload, + payer_email=payer_email, + payment_method=candidate, + ) + pal24_payment_method = candidate + break + except Pal24APIError as error: + last_error = error + logger.warning( + "Pal24 отклонил способ оплаты %s для пользователя %s: %s", + candidate or "", + user_id, + error, + ) + + if response is None: + if last_error: + logger.error( + "Ошибка Pal24 API при создании счета (метод %s): %s", + normalized_payment_method, + last_error, + ) return None if not response.get("success", True): @@ -148,6 +168,7 @@ class Pal24PaymentMixin: "links": metadata_links, "raw_response": response, "selected_method": normalized_payment_method, + "provider_method": pal24_payment_method, } payment = await payment_module.create_pal24_payment( @@ -191,6 +212,7 @@ class Pal24PaymentMixin: "transfer_url": transfer_url, "link_page_url": link_page_url, "payment_url": primary_link, + "provider_payment_method": pal24_payment_method, } async def process_pal24_postback( @@ -528,3 +550,28 @@ class Pal24PaymentMixin: normalized = payment_method.strip().lower() return mapping.get(normalized, "sbp") + + @staticmethod + def _get_payment_method_candidates(normalized_method: str) -> list[Optional[str]]: + mapping = { + "sbp": [ + "fastpay", + "fast_payment", + "fastpayment", + "sbp", + "fast_payment_system", + ], + "card": [ + "bank_card", + "card", + ], + } + + candidates = [ + option + for option in mapping.get(normalized_method, []) + if option not in (None, "") + ] + + candidates.append(None) + return candidates diff --git a/tests/services/test_payment_service_pal24.py b/tests/services/test_payment_service_pal24.py index 0e588b53..2e05c8c4 100644 --- a/tests/services/test_payment_service_pal24.py +++ b/tests/services/test_payment_service_pal24.py @@ -34,7 +34,13 @@ class DummyLocalPayment: class StubPal24Service: - def __init__(self, *, configured: bool = True, response: Optional[Dict[str, Any]] = None) -> None: + def __init__( + self, + *, + configured: bool = True, + response: Optional[Dict[str, Any]] = None, + fail_methods: Optional[set[Optional[str]]] = None, + ) -> None: self.is_configured = configured self.response = response or { "success": True, @@ -45,9 +51,12 @@ class StubPal24Service: } self.calls: list[Dict[str, Any]] = [] self.raise_error: Optional[Exception] = None + self.fail_methods = fail_methods or set() async def create_bill(self, **kwargs: Any) -> Dict[str, Any]: self.calls.append(kwargs) + if kwargs.get("payment_method") in self.fail_methods: + raise Pal24APIError("invalid payment method") if self.raise_error: raise self.raise_error return self.response @@ -105,7 +114,106 @@ async def test_create_pal24_payment_success(monkeypatch: pytest.MonkeyPatch) -> assert result["link_url"] == "https://pal24/sbp" assert result["card_url"] == "https://pal24/card" assert stub.calls and stub.calls[0]["amount_kopeks"] == 50000 + assert stub.calls[0]["payment_method"] == "bank_card" assert "links" in captured_args["metadata"] + assert captured_args["metadata"]["provider_method"] == "bank_card" + + +@pytest.mark.anyio("asyncio") +async def test_create_pal24_payment_default_method(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPal24Service() + service = _make_service(stub) + db = DummySession() + + async def fake_create_pal24_payment(*args: Any, **kwargs: Any) -> DummyLocalPayment: + return DummyLocalPayment(payment_id=111) + + monkeypatch.setattr( + payment_service_module, + "create_pal24_payment", + fake_create_pal24_payment, + raising=False, + ) + monkeypatch.setattr(settings, "PAL24_MIN_AMOUNT_KOPEKS", 1000, raising=False) + monkeypatch.setattr(settings, "PAL24_MAX_AMOUNT_KOPEKS", 1_000_000, raising=False) + + result = await service.create_pal24_payment( + db=db, + user_id=42, + amount_kopeks=150000, + description="Пополнение", + language="ru", + ) + + assert result is not None + assert result["payment_method"] == "sbp" + assert stub.calls and stub.calls[0]["payment_method"] == "fastpay" + + +@pytest.mark.anyio("asyncio") +async def test_create_pal24_payment_retries_on_invalid_method(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPal24Service(fail_methods={"fastpay"}) + service = _make_service(stub) + db = DummySession() + + async def fake_create_pal24_payment(*args: Any, **kwargs: Any) -> DummyLocalPayment: + return DummyLocalPayment(payment_id=222) + + monkeypatch.setattr( + payment_service_module, + "create_pal24_payment", + fake_create_pal24_payment, + raising=False, + ) + monkeypatch.setattr(settings, "PAL24_MIN_AMOUNT_KOPEKS", 1000, raising=False) + monkeypatch.setattr(settings, "PAL24_MAX_AMOUNT_KOPEKS", 1_000_000, raising=False) + + result = await service.create_pal24_payment( + db=db, + user_id=77, + amount_kopeks=250000, + description="Пополнение", + language="ru", + payment_method="sbp", + ) + + assert result is not None + assert result["local_payment_id"] == 222 + assert len(stub.calls) == 2 + assert stub.calls[0]["payment_method"] == "fastpay" + assert stub.calls[1]["payment_method"] == "fast_payment" + assert result["provider_payment_method"] == "fast_payment" + + +@pytest.mark.anyio("asyncio") +async def test_create_pal24_payment_returns_none_if_all_methods_fail(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPal24Service( + fail_methods={ + "fastpay", + "fast_payment", + "fastpayment", + "fast_payment_system", + "sbp", + None, + } + ) + service = _make_service(stub) + db = DummySession() + + monkeypatch.setattr(settings, "PAL24_MIN_AMOUNT_KOPEKS", 1000, raising=False) + monkeypatch.setattr(settings, "PAL24_MAX_AMOUNT_KOPEKS", 1_000_000, raising=False) + + result = await service.create_pal24_payment( + db=db, + user_id=13, + amount_kopeks=120000, + description="Пополнение", + language="ru", + payment_method="sbp", + ) + + assert result is None + assert len(stub.calls) == 6 @pytest.mark.anyio("asyncio")