diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 3d4bebfc..3237fa10 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -61,7 +61,7 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { }, "payments": { "title": "πŸ’³ ΠŸΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Π΅ систСмы", - "description": "YooKassa, CryptoBot, Heleket, MulenPay, PAL24, Wata, Tribute ΠΈ Telegram Stars.", + "description": "YooKassa, CryptoBot, Heleket, MulenPay, PAL24, Wata, Platega, Tribute ΠΈ Telegram Stars.", "icon": "πŸ’³", "categories": ( "PAYMENT", @@ -72,6 +72,7 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { "MULENPAY", "PAL24", "WATA", + "PLATEGA", "TRIBUTE", "TELEGRAM", ), @@ -253,6 +254,7 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: payment_statuses = { "YooKassa": settings.is_yookassa_enabled(), "CryptoBot": settings.is_cryptobot_enabled(), + "Platega": settings.is_platega_enabled(), "MulenPay": settings.is_mulenpay_enabled(), "PAL24": settings.is_pal24_enabled(), "Tribute": settings.TRIBUTE_ENABLED, diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 8e8f5e42..2ba734f3 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -84,6 +84,7 @@ class BotConfigurationService: "CRYPTOBOT": "πŸͺ™ CryptoBot", "HELEKET": "πŸͺ™ Heleket", "YOOKASSA": "🟣 YooKassa", + "PLATEGA": "πŸ’³ Platega", "TRIBUTE": "🎁 Tribute", "MULENPAY": "πŸ’° {mulenpay_name}", "PAL24": "🏦 PAL24 / PayPalych", @@ -137,6 +138,7 @@ class BotConfigurationService: "YOOKASSA": "Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с YooKassa: ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€Ρ‹ ΠΌΠ°Π³Π°Π·ΠΈΠ½Π° ΠΈ Π²Π΅Π±Ρ…ΡƒΠΊΠΈ.", "CRYPTOBOT": "CryptoBot ΠΈ ΠΊΡ€ΠΈΠΏΡ‚ΠΎΠΏΠ»Π°Ρ‚Π΅ΠΆΠΈ Ρ‡Π΅Ρ€Π΅Π· Telegram.", "HELEKET": "Heleket: ΠΊΡ€ΠΈΠΏΡ‚ΠΎΠΏΠ»Π°Ρ‚Π΅ΠΆΠΈ, ΠΊΠ»ΡŽΡ‡ΠΈ ΠΌΠ΅Ρ€Ρ‡Π°Π½Ρ‚Π° ΠΈ Π²Π΅Π±Ρ…ΡƒΠΊΠΈ.", + "PLATEGA": "Platega: merchant ID, сСкрСт, ссылки Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π° ΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ ΠΎΠΏΠ»Π°Ρ‚Ρ‹.", "MULENPAY": "ΠŸΠ»Π°Ρ‚Π΅ΠΆΠΈ {mulenpay_name} ΠΈ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ ΠΌΠ°Π³Π°Π·ΠΈΠ½Π°.", "PAL24": "PAL24 / PayPalych ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΈ Π»ΠΈΠΌΠΈΡ‚Ρ‹.", "TRIBUTE": "Tribute ΠΈ Π΄ΠΎΠ½Π°Ρ‚-сСрвисы.", @@ -303,6 +305,7 @@ class BotConfigurationService: "YOOKASSA_": "YOOKASSA", "CRYPTOBOT_": "CRYPTOBOT", "HELEKET_": "HELEKET", + "PLATEGA_": "PLATEGA", "MULENPAY_": "MULENPAY", "PAL24_": "PAL24", "PAYMENT_": "PAYMENT", diff --git a/tests/services/test_payment_service_platega.py b/tests/services/test_payment_service_platega.py new file mode 100644 index 00000000..373cc8ca --- /dev/null +++ b/tests/services/test_payment_service_platega.py @@ -0,0 +1,232 @@ +"""ВСсты для сцСнариСв Platega Π² PaymentService.""" + +from __future__ import annotations + +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional +import sys + +import pytest + +ROOT_DIR = Path(__file__).resolve().parents[2] +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + +import app.services.payment_service as payment_service_module # noqa: E402 +from app.config import settings # noqa: E402 +from app.services.payment_service import PaymentService # noqa: E402 + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +class DummySession: + async def commit(self) -> None: # pragma: no cover - no custom logic required + return None + + async def refresh(self, *_: Any) -> None: # pragma: no cover - no custom logic required + return None + + +class DummyLocalPayment: + def __init__(self, payment_id: int = 101) -> None: + self.id = payment_id + self.created_at = datetime.utcnow() + + +class StubPlategaService: + def __init__( + self, + *, + configured: bool = True, + response: Optional[Dict[str, Any]] = None, + transaction_payload: Optional[Dict[str, Any]] = None, + ) -> None: + self.is_configured = configured + self.response = response or { + "transactionId": "trx-001", + "redirect": "https://platega.example/pay", + "status": "PENDING", + "expiresIn": 900, + } + self.transaction_payload = transaction_payload + self.calls: list[Dict[str, Any]] = [] + self.raise_error: Optional[Exception] = None + + async def create_payment(self, **kwargs: Any) -> Optional[Dict[str, Any]]: + self.calls.append(kwargs) + if self.raise_error: + raise self.raise_error + return self.response + + async def get_transaction(self, transaction_id: str) -> Optional[Dict[str, Any]]: + self.calls.append({"transaction_lookup": transaction_id}) + return self.transaction_payload + + +def _make_service(stub: Optional[StubPlategaService]) -> PaymentService: + service = PaymentService.__new__(PaymentService) # type: ignore[call-arg] + service.bot = None + service.platega_service = stub + service.yookassa_service = None + service.cryptobot_service = None + service.heleket_service = None + service.mulenpay_service = None + service.pal24_service = None + service.stars_service = None + service.wata_service = None + return service + + +@pytest.mark.anyio("asyncio") +async def test_create_platega_payment_success(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPlategaService() + service = _make_service(stub) + db = DummySession() + + captured_args: Dict[str, Any] = {} + + async def fake_create_platega_payment(*args: Any, **kwargs: Any) -> DummyLocalPayment: + if args: + captured_args["db_arg"] = args[0] + captured_args.update(kwargs) + return DummyLocalPayment(payment_id=777) + + monkeypatch.setattr( + payment_service_module, + "create_platega_payment", + fake_create_platega_payment, + raising=False, + ) + monkeypatch.setattr(settings, "PLATEGA_MIN_AMOUNT_KOPEKS", 10_000, raising=False) + monkeypatch.setattr(settings, "PLATEGA_MAX_AMOUNT_KOPEKS", 500_000, raising=False) + monkeypatch.setattr(settings, "PLATEGA_CURRENCY", "RUB", raising=False) + monkeypatch.setattr(settings, "PLATEGA_RETURN_URL", "https://return", raising=False) + monkeypatch.setattr(settings, "PLATEGA_FAILED_URL", "https://failed", raising=False) + + result = await service.create_platega_payment( + db=db, + user_id=42, + amount_kopeks=50_000, + description="ПополнСниС счёта", + language="ru", + payment_method_code=10, + ) + + assert result is not None + assert result["local_payment_id"] == 777 + assert result["transaction_id"] == "trx-001" + assert result["redirect_url"] == "https://platega.example/pay" + assert result["status"] == "PENDING" + assert "correlation_id" in result and len(result["correlation_id"]) == 32 + assert captured_args["user_id"] == 42 + assert captured_args["amount_kopeks"] == 50_000 + assert captured_args["payment_method_code"] == 10 + assert captured_args["metadata"]["selected_method"] == 10 + assert stub.calls and stub.calls[0]["payment_method"] == 10 + assert stub.calls[0]["amount"] == pytest.approx(500.0) + assert stub.calls[0]["currency"] == "RUB" + assert captured_args["metadata"]["language"] == "ru" + + +@pytest.mark.anyio("asyncio") +async def test_create_platega_payment_respects_limits_and_configuration(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPlategaService() + service = _make_service(stub) + db = DummySession() + + monkeypatch.setattr(settings, "PLATEGA_MIN_AMOUNT_KOPEKS", 20_000, raising=False) + monkeypatch.setattr(settings, "PLATEGA_MAX_AMOUNT_KOPEKS", 40_000, raising=False) + + too_low = await service.create_platega_payment( + db=db, + user_id=1, + amount_kopeks=10_000, + description="ПополнСниС", + language="ru", + payment_method_code=2, + ) + assert too_low is None + + too_high = await service.create_platega_payment( + db=db, + user_id=1, + amount_kopeks=100_000, + description="ПополнСниС", + language="ru", + payment_method_code=2, + ) + assert too_high is None + + not_configured_service = _make_service(StubPlategaService(configured=False)) + result = await not_configured_service.create_platega_payment( + db=db, + user_id=1, + amount_kopeks=30_000, + description="ПополнСниС", + language="ru", + payment_method_code=2, + ) + assert result is None + + +@pytest.mark.anyio("asyncio") +async def test_create_platega_payment_handles_service_errors(monkeypatch: pytest.MonkeyPatch) -> None: + stub = StubPlategaService() + stub.raise_error = RuntimeError("network down") + service = _make_service(stub) + db = DummySession() + + async def fake_create_platega_payment(*_: Any, **__: Any) -> DummyLocalPayment: + pytest.fail("local payment must not be created when Platega call fails") + + monkeypatch.setattr( + payment_service_module, + "create_platega_payment", + fake_create_platega_payment, + raising=False, + ) + monkeypatch.setattr(settings, "PLATEGA_MIN_AMOUNT_KOPEKS", 1_000, raising=False) + monkeypatch.setattr(settings, "PLATEGA_MAX_AMOUNT_KOPEKS", 1_000_000, raising=False) + + result = await service.create_platega_payment( + db=db, + user_id=5, + amount_kopeks=25_000, + description="ПополнСниС", + language="ru", + payment_method_code=13, + ) + assert result is None + assert stub.calls and "payment_method" in stub.calls[0] + + +def test_get_platega_active_methods_parses_and_filters(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + settings, + "PLATEGA_ACTIVE_METHODS", + " 2,10, 11 ;12,13,13,invalid ", + raising=False, + ) + + methods = settings.get_platega_active_methods() + + assert methods == [2, 10, 11, 12, 13] + + +def test_get_platega_active_methods_returns_default(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "PLATEGA_ACTIVE_METHODS", "", raising=False) + + methods = settings.get_platega_active_methods() + + assert methods == [2] + + +def test_platega_method_display_helpers() -> None: + assert settings.get_platega_method_display_name(10) == "БанковскиС ΠΊΠ°Ρ€Ρ‚Ρ‹ (RUB)" + assert settings.get_platega_method_display_title(10) == "πŸ’³ ΠšΠ°Ρ€Ρ‚Ρ‹ (RUB)" + assert settings.get_platega_method_display_name(999) == "ΠœΠ΅Ρ‚ΠΎΠ΄ 999" + assert settings.get_platega_method_display_title(999) == "Platega 999"