Files
remnawave-bedolaga-telegram…/tests/services/test_payment_service_pal24.py

332 lines
10 KiB
Python

"""Тесты Pal24 сценариев PaymentService."""
from pathlib import Path
from typing import Any, Dict, Optional
import sys
from datetime import datetime
from types import SimpleNamespace
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
from app.services.pal24_service import Pal24APIError # noqa: E402
@pytest.fixture
def anyio_backend() -> str:
return "asyncio"
class DummySession:
async def commit(self) -> None: # pragma: no cover
return None
class DummyLocalPayment:
def __init__(self, payment_id: int = 404) -> None:
self.id = payment_id
self.created_at = datetime(2024, 1, 2, 10, 0, 0)
class StubPal24Service:
def __init__(
self,
*,
configured: bool = True,
response: Optional[Dict[str, Any]] = None,
) -> None:
self.is_configured = configured
self.response = response or {
"success": True,
"bill_id": "BILL-1",
"transfer_url": "https://pal24/sbp",
"link_url": "https://pal24/card",
"status": "NEW",
}
self.calls: list[Dict[str, Any]] = []
self.raise_error: Optional[Exception] = None
self.status_response: Optional[Dict[str, Any]] = {"status": "NEW"}
self.payment_status_response: Optional[Dict[str, Any]] = None
self.bill_payments_response: Optional[Dict[str, Any]] = None
self.status_calls: list[str] = []
self.payment_status_calls: list[str] = []
self.bill_payments_calls: list[str] = []
async def create_bill(self, **kwargs: Any) -> Dict[str, Any]:
self.calls.append(kwargs)
if self.raise_error:
raise self.raise_error
return self.response
async def get_bill_status(self, bill_id: str) -> Optional[Dict[str, Any]]:
self.status_calls.append(bill_id)
return self.status_response
async def get_payment_status(self, payment_id: str) -> Optional[Dict[str, Any]]:
self.payment_status_calls.append(payment_id)
return self.payment_status_response
async def get_bill_payments(self, bill_id: str) -> Optional[Dict[str, Any]]:
self.bill_payments_calls.append(bill_id)
return self.bill_payments_response
def _make_service(stub: Optional[StubPal24Service]) -> PaymentService:
service = PaymentService.__new__(PaymentService) # type: ignore[call-arg]
service.bot = None
service.pal24_service = stub
service.mulenpay_service = None
service.yookassa_service = None
service.cryptobot_service = None
service.stars_service = None
service.heleket_service = None
return service
@pytest.mark.anyio("asyncio")
async def test_create_pal24_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
stub = StubPal24Service()
service = _make_service(stub)
db = DummySession()
captured_args: Dict[str, Any] = {}
async def fake_create_pal24_payment(*args: Any, **kwargs: Any) -> DummyLocalPayment:
captured_args.update(kwargs)
if args:
captured_args["db_arg"] = args[0]
return DummyLocalPayment(payment_id=321)
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=15,
amount_kopeks=50000,
description="Оплата подписки",
language="ru",
ttl_seconds=600,
payer_email="user@example.com",
payment_method="card",
)
assert result is not None
assert result["local_payment_id"] == 321
assert result["bill_id"] == "BILL-1"
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"] == "SBP"
assert result["payment_method"] == "sbp"
@pytest.mark.anyio("asyncio")
async def test_create_pal24_payment_limits_and_configuration(monkeypatch: pytest.MonkeyPatch) -> None:
stub = StubPal24Service()
service = _make_service(stub)
db = DummySession()
monkeypatch.setattr(settings, "PAL24_MIN_AMOUNT_KOPEKS", 5000, raising=False)
monkeypatch.setattr(settings, "PAL24_MAX_AMOUNT_KOPEKS", 20_000, raising=False)
result_low = await service.create_pal24_payment(
db=db,
user_id=1,
amount_kopeks=1000,
description="Пополнение",
language="ru",
)
assert result_low is None
result_high = await service.create_pal24_payment(
db=db,
user_id=1,
amount_kopeks=50_000,
description="Пополнение",
language="ru",
)
assert result_high is None
service_not_configured = _make_service(StubPal24Service(configured=False))
result_config = await service_not_configured.create_pal24_payment(
db=db,
user_id=1,
amount_kopeks=10_000,
description="Пополнение",
language="ru",
)
assert result_config is None
@pytest.mark.anyio("asyncio")
async def test_create_pal24_payment_handles_api_errors(monkeypatch: pytest.MonkeyPatch) -> None:
stub = StubPal24Service()
stub.raise_error = Pal24APIError("api failed")
service = _make_service(stub)
db = DummySession()
monkeypatch.setattr(settings, "PAL24_MIN_AMOUNT_KOPEKS", 1000, raising=False)
monkeypatch.setattr(settings, "PAL24_MAX_AMOUNT_KOPEKS", 10_000, raising=False)
result = await service.create_pal24_payment(
db=db,
user_id=5,
amount_kopeks=2000,
description="Пополнение",
language="ru",
)
assert result is None
@pytest.mark.anyio("asyncio")
async def test_get_pal24_payment_status_updates_from_remote(monkeypatch: pytest.MonkeyPatch) -> None:
stub = StubPal24Service()
stub.status_response = {"status": "SUCCESS"}
stub.payment_status_response = {
"success": True,
"id": "PAY-1",
"bill_id": "BILL-1",
"status": "SUCCESS",
"payment_method": "SBP",
"account_amount": "700.00",
"from_card": "676754******1234",
}
stub.bill_payments_response = {
"data": [
{
"id": "PAY-1",
"bill_id": "BILL-1",
"status": "SUCCESS",
"from_card": "676754******1234",
"payment_method": "SBP",
}
]
}
service = _make_service(stub)
db = DummySession()
payment = SimpleNamespace(
id=99,
bill_id="BILL-1",
payment_id=None,
payment_status="NEW",
payment_method=None,
balance_amount=None,
balance_currency=None,
payer_account=None,
status="NEW",
is_paid=False,
paid_at=None,
transaction_id=None,
user_id=1,
)
async def fake_get_by_id(db: DummySession, payment_id: int) -> SimpleNamespace:
assert payment_id == payment.id
return payment
async def fake_update_status(
db: DummySession,
payment_obj: SimpleNamespace,
*,
status: str,
**kwargs: Any,
) -> SimpleNamespace:
payment_obj.status = status
payment_obj.last_status = status
for key, value in kwargs.items():
setattr(payment_obj, key, value)
if "is_paid" in kwargs:
payment_obj.is_paid = kwargs["is_paid"]
await db.commit()
return payment_obj
async def fake_finalize(
self: PaymentService,
db: DummySession,
payment_obj: Any,
*,
payment_id: Optional[str] = None,
trigger: str,
) -> bool:
return False
monkeypatch.setattr(
payment_service_module,
"get_pal24_payment_by_id",
fake_get_by_id,
raising=False,
)
monkeypatch.setattr(
payment_service_module,
"update_pal24_payment_status",
fake_update_status,
raising=False,
)
monkeypatch.setattr(
PaymentService,
"_finalize_pal24_payment",
fake_finalize,
raising=False,
)
result = await service.get_pal24_payment_status(db, local_payment_id=payment.id)
assert result is not None
assert payment.status == "SUCCESS"
assert payment.payment_id == "PAY-1"
assert payment.payment_status == "SUCCESS"
assert payment.payment_method == "sbp"
assert payment.is_paid is True
assert stub.status_calls == ["BILL-1"]
assert stub.payment_status_calls in ([], ["PAY-1"])
assert result["remote_status"] == "SUCCESS"
assert result["remote_data"] and "bill_status" in result["remote_data"]