Files
remnawave-bedolaga-telegram…/tests/services/test_payment_service_pal24.py
T
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00

333 lines
10 KiB
Python

"""Тесты Pal24 сценариев PaymentService."""
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from typing import Any
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
from app.config import settings
from app.services.pal24_service import Pal24APIError
from app.services.payment_service import PaymentService
@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: dict[str, Any] | None = 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: Exception | None = None
self.status_response: dict[str, Any] | None = {'status': 'NEW'}
self.payment_status_response: dict[str, Any] | None = None
self.bill_payments_response: dict[str, Any] | None = 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) -> dict[str, Any] | None:
self.status_calls.append(bill_id)
return self.status_response
async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
self.payment_status_calls.append(payment_id)
return self.payment_status_response
async def get_bill_payments(self, bill_id: str) -> dict[str, Any] | None:
self.bill_payments_calls.append(bill_id)
return self.bill_payments_response
def _make_service(stub: StubPal24Service | None) -> 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: str | None = 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']