From 6dfd7db1152c250c594d6572420f1b50ec1b5975 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 16 Oct 2025 16:08:28 +0300 Subject: [PATCH 1/4] Revert "Fix Pal24 payment method mapping" --- app/services/payment/pal24.py | 12 +------- tests/services/test_payment_service_pal24.py | 32 -------------------- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 7b1d9a1e..2283aca2 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -64,7 +64,6 @@ class Pal24PaymentMixin: } normalized_payment_method = self._normalize_payment_method(payment_method) - pal24_payment_method = self._map_payment_method_for_api(normalized_payment_method) payment_module = import_module("app.services.payment_service") @@ -77,7 +76,7 @@ class Pal24PaymentMixin: ttl_seconds=ttl_seconds, custom_payload=custom_payload, payer_email=payer_email, - payment_method=pal24_payment_method, + payment_method=normalized_payment_method, ) except Pal24APIError as error: logger.error("Ошибка Pal24 API при создании счета: %s", error) @@ -529,12 +528,3 @@ class Pal24PaymentMixin: normalized = payment_method.strip().lower() return mapping.get(normalized, "sbp") - - @staticmethod - def _map_payment_method_for_api(normalized_method: str) -> Optional[str]: - mapping = { - "sbp": "fast_payment", - "card": "bank_card", - } - - return mapping.get(normalized_method) diff --git a/tests/services/test_payment_service_pal24.py b/tests/services/test_payment_service_pal24.py index b3e820f5..0e588b53 100644 --- a/tests/services/test_payment_service_pal24.py +++ b/tests/services/test_payment_service_pal24.py @@ -105,41 +105,9 @@ 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"] -@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"] == "fast_payment" - - @pytest.mark.anyio("asyncio") async def test_create_pal24_payment_limits_and_configuration(monkeypatch: pytest.MonkeyPatch) -> None: stub = StubPal24Service() From e229b625f6ff2d755dd8af5d4fbdd7073a9da0d2 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 16 Oct 2025 16:19:24 +0300 Subject: [PATCH 2/4] 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") From d4df56b1735ed1478fd6885af19ca1b5002bf832 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 16 Oct 2025 16:29:07 +0300 Subject: [PATCH 3/4] Revert "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, 15 insertions(+), 170 deletions(-) diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 7ec2fcad..2283aca2 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -64,42 +64,22 @@ class Pal24PaymentMixin: } normalized_payment_method = self._normalize_payment_method(payment_method) + payment_module = import_module("app.services.payment_service") - 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, - ) + 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) return None if not response.get("success", True): @@ -168,7 +148,6 @@ 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( @@ -212,7 +191,6 @@ 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( @@ -550,28 +528,3 @@ 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 2e05c8c4..0e588b53 100644 --- a/tests/services/test_payment_service_pal24.py +++ b/tests/services/test_payment_service_pal24.py @@ -34,13 +34,7 @@ class DummyLocalPayment: class StubPal24Service: - def __init__( - self, - *, - configured: bool = True, - response: Optional[Dict[str, Any]] = None, - fail_methods: Optional[set[Optional[str]]] = None, - ) -> None: + def __init__(self, *, configured: bool = True, response: Optional[Dict[str, Any]] = None) -> None: self.is_configured = configured self.response = response or { "success": True, @@ -51,12 +45,9 @@ 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 @@ -114,106 +105,7 @@ 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") From 91a3b14a1f7671c75504baa4c357f39d23e1c0d5 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 16 Oct 2025 16:36:56 +0300 Subject: [PATCH 4/4] Fix Pal24 payment method mapping --- app/services/payment/pal24.py | 14 ++++++++- tests/services/test_payment_service_pal24.py | 33 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 2283aca2..ab2da960 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -64,6 +64,7 @@ class Pal24PaymentMixin: } normalized_payment_method = self._normalize_payment_method(payment_method) + api_payment_method = self._map_api_payment_method(normalized_payment_method) payment_module = import_module("app.services.payment_service") @@ -76,7 +77,7 @@ class Pal24PaymentMixin: ttl_seconds=ttl_seconds, custom_payload=custom_payload, payer_email=payer_email, - payment_method=normalized_payment_method, + payment_method=api_payment_method, ) except Pal24APIError as error: logger.error("Ошибка Pal24 API при создании счета: %s", error) @@ -528,3 +529,14 @@ class Pal24PaymentMixin: normalized = payment_method.strip().lower() return mapping.get(normalized, "sbp") + + @staticmethod + def _map_api_payment_method(normalized_payment_method: str) -> Optional[str]: + """Преобразует нормализованный метод оплаты в значение для Pal24 API.""" + + api_mapping = { + "sbp": "fast_payment", + "card": "bank_card", + } + + return api_mapping.get(normalized_payment_method) diff --git a/tests/services/test_payment_service_pal24.py b/tests/services/test_payment_service_pal24.py index 0e588b53..e93bf929 100644 --- a/tests/services/test_payment_service_pal24.py +++ b/tests/services/test_payment_service_pal24.py @@ -104,10 +104,43 @@ async def test_create_pal24_payment_success(monkeypatch: pytest.MonkeyPatch) -> assert result["payment_method"] == "card" assert result["link_url"] == "https://pal24/sbp" assert result["card_url"] == "https://pal24/card" + assert stub.calls and stub.calls[0]["payment_method"] == "bank_card" assert stub.calls and stub.calls[0]["amount_kopeks"] == 50000 assert "links" in captured_args["metadata"] +@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=10_000, + description="Пополнение", + language="ru", + ) + + assert result is not None + assert stub.calls and stub.calls[0]["payment_method"] == "fast_payment" + assert result["payment_method"] == "sbp" + + @pytest.mark.anyio("asyncio") async def test_create_pal24_payment_limits_and_configuration(monkeypatch: pytest.MonkeyPatch) -> None: stub = StubPal24Service()