"""Tests for WATA payment mixin.""" from __future__ import annotations from datetime import datetime, timezone from pathlib import Path import sys from typing import Any, Dict, Optional 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.wata_service import WataService # noqa: E402 @pytest.fixture def anyio_backend() -> str: return "asyncio" class DummySession: async def commit(self) -> None: # pragma: no cover - no logic required return None async def refresh(self, *_: Any) -> None: # pragma: no cover - no logic required return None class DummyLocalPayment: def __init__(self, payment_id: int = 42) -> None: self.id = payment_id self.created_at = datetime.utcnow() class StubWataService: def __init__(self, response: Optional[Dict[str, Any]]) -> None: self.response = response self.calls: list[Dict[str, Any]] = [] async def create_payment_link(self, **kwargs: Any) -> Optional[Dict[str, Any]]: self.calls.append(kwargs) return self.response class DummyWataPayment: def __init__(self) -> None: self.id = 1 self.user_id = 42 self.payment_link_id = "link-123" self.order_id = "order-123" self.amount_kopeks = 15_000 self.currency = "RUB" self.description = "Пополнение" self.status = "Opened" self.is_paid = False self.metadata_json: Dict[str, Any] = {} self.transaction_id: Optional[int] = None self.callback_payload: Optional[Dict[str, Any]] = None self.terminal_public_id: Optional[str] = None def _make_service(stub: Optional[StubWataService]) -> PaymentService: service = PaymentService.__new__(PaymentService) # type: ignore[call-arg] service.bot = None service.wata_service = stub service.mulenpay_service = None service.pal24_service = None service.yookassa_service = None service.stars_service = None service.cryptobot_service = None service.heleket_service = None return service def test_wata_service_format_datetime_accepts_naive_utc() -> None: value = datetime(2024, 5, 20, 12, 30, 0) formatted = WataService._format_datetime(value) assert formatted == "2024-05-20T12:30:00Z" def test_wata_service_parse_datetime_returns_naive_utc() -> None: parsed = WataService._parse_datetime("2024-05-20T12:30:00Z") assert parsed == datetime(2024, 5, 20, 12, 30, 0) assert parsed.tzinfo is None parsed_with_offset = WataService._parse_datetime("2024-05-20T15:30:00+03:00") assert parsed_with_offset == datetime(2024, 5, 20, 12, 30, 0) assert parsed_with_offset.tzinfo is None @pytest.mark.anyio("asyncio") async def test_create_wata_payment_success(monkeypatch: pytest.MonkeyPatch) -> None: response = { "id": "123e4567-e89b-12d3-a456-426614174000", "url": "https://wata.example/link", "status": "Opened", "type": "OneTime", "terminalPublicId": "terminal-id", "successRedirectUrl": "https://example.com/success", "failRedirectUrl": "https://example.com/fail", "expirationDateTime": "2030-01-01T00:00:00Z", } stub = StubWataService(response) service = _make_service(stub) db = DummySession() captured_args: Dict[str, Any] = {} async def fake_create_wata_payment(**kwargs: Any) -> DummyLocalPayment: captured_args.update(kwargs) return DummyLocalPayment(payment_id=777) monkeypatch.setattr(payment_service_module, "create_wata_payment", fake_create_wata_payment, raising=False) monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 5000, raising=False) monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 500_000, raising=False) result = await service.create_wata_payment( db=db, user_id=101, amount_kopeks=15000, description="Пополнение", language="ru", ) assert result is not None assert result["local_payment_id"] == 777 assert result["payment_link_id"] == response["id"] assert result["payment_url"] == response["url"] assert captured_args["user_id"] == 101 assert captured_args["amount_kopeks"] == 15000 assert captured_args["payment_link_id"] == response["id"] assert stub.calls and stub.calls[0]["amount_kopeks"] == 15000 @pytest.mark.anyio("asyncio") async def test_create_wata_payment_respects_amount_limits(monkeypatch: pytest.MonkeyPatch) -> None: stub = StubWataService({"id": "link"}) service = _make_service(stub) db = DummySession() monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 10_000, raising=False) monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 20_000, raising=False) too_low = await service.create_wata_payment( db=db, user_id=1, amount_kopeks=5_000, description="Пополнение", ) assert too_low is None too_high = await service.create_wata_payment( db=db, user_id=1, amount_kopeks=25_000, description="Пополнение", ) assert too_high is None assert not stub.calls @pytest.mark.anyio("asyncio") async def test_create_wata_payment_returns_none_without_service() -> None: service = _make_service(None) db = DummySession() result = await service.create_wata_payment( db=db, user_id=5, amount_kopeks=10_000, description="Пополнение", ) assert result is None @pytest.mark.anyio("asyncio") async def test_process_wata_webhook_updates_status(monkeypatch: pytest.MonkeyPatch) -> None: service = _make_service(None) db = DummySession() payment = DummyWataPayment() update_kwargs: Dict[str, Any] = {} link_lookup_called = False async def fake_get_by_order_id(db_arg: Any, order_id: str) -> DummyWataPayment: assert db_arg is db assert order_id == payment.order_id return payment async def fake_get_by_link_id(*_: Any, **__: Any) -> Optional[DummyWataPayment]: nonlocal link_lookup_called link_lookup_called = True return None async def fake_update_status( db_arg: Any, *, payment: DummyWataPayment, **kwargs: Any, ) -> DummyWataPayment: assert db_arg is db update_kwargs.update(kwargs) if "status" in kwargs: payment.status = kwargs["status"] if "is_paid" in kwargs: payment.is_paid = kwargs["is_paid"] if "metadata" in kwargs: payment.metadata_json = kwargs["metadata"] if "callback_payload" in kwargs: payment.callback_payload = kwargs["callback_payload"] if "terminal_public_id" in kwargs: payment.terminal_public_id = kwargs["terminal_public_id"] return payment monkeypatch.setattr( payment_service_module, "get_wata_payment_by_order_id", fake_get_by_order_id, raising=False, ) monkeypatch.setattr( payment_service_module, "get_wata_payment_by_link_id", fake_get_by_link_id, raising=False, ) monkeypatch.setattr( payment_service_module, "update_wata_payment_status", fake_update_status, raising=False, ) payload = { "orderId": payment.order_id, "transactionStatus": "Declined", "terminalPublicId": "terminal-001", } processed = await service.process_wata_webhook(db, payload) assert processed is True assert link_lookup_called is False assert payment.status == "Declined" assert payment.is_paid is False assert payment.metadata_json.get("last_webhook") == payload assert payment.callback_payload == payload assert payment.terminal_public_id == "terminal-001" assert update_kwargs["status"] == "Declined" assert update_kwargs["is_paid"] is False @pytest.mark.anyio("asyncio") async def test_process_wata_webhook_finalizes_paid(monkeypatch: pytest.MonkeyPatch) -> None: service = _make_service(None) db = DummySession() payment = DummyWataPayment() finalize_called = False async def fake_get_by_order_id(*_: Any, **__: Any) -> DummyWataPayment: return payment async def fake_update_status( db_arg: Any, *, payment: DummyWataPayment, **kwargs: Any, ) -> DummyWataPayment: if "metadata" in kwargs: payment.metadata_json = kwargs["metadata"] if "callback_payload" in kwargs: payment.callback_payload = kwargs["callback_payload"] if "status" in kwargs: payment.status = kwargs["status"] return payment async def fake_finalize( db_arg: Any, payment_arg: DummyWataPayment, payload_arg: Dict[str, Any], ) -> DummyWataPayment: nonlocal finalize_called finalize_called = True payment_arg.is_paid = True return payment_arg monkeypatch.setattr( payment_service_module, "get_wata_payment_by_order_id", fake_get_by_order_id, raising=False, ) monkeypatch.setattr( payment_service_module, "get_wata_payment_by_link_id", lambda *args, **kwargs: None, raising=False, ) monkeypatch.setattr( payment_service_module, "update_wata_payment_status", fake_update_status, raising=False, ) monkeypatch.setattr( service, "_finalize_wata_payment", fake_finalize, raising=False, ) payload = { "orderId": payment.order_id, "transactionStatus": "Paid", "transactionId": "tx-001", } processed = await service.process_wata_webhook(db, payload) assert processed is True assert finalize_called is True assert payment.is_paid is True assert payment.metadata_json.get("last_webhook") == payload @pytest.mark.anyio("asyncio") async def test_process_wata_webhook_returns_false_when_payment_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: service = _make_service(None) db = DummySession() async def fake_get_by_order_id(*_: Any, **__: Any) -> None: return None async def fake_get_by_link_id(*_: Any, **__: Any) -> None: return None async def fail_update(*_: Any, **__: Any) -> None: pytest.fail("update_wata_payment_status should not be called") monkeypatch.setattr( payment_service_module, "get_wata_payment_by_order_id", fake_get_by_order_id, raising=False, ) monkeypatch.setattr( payment_service_module, "get_wata_payment_by_link_id", fake_get_by_link_id, raising=False, ) monkeypatch.setattr( payment_service_module, "update_wata_payment_status", fail_update, raising=False, ) payload = { "orderId": "missing-order", "transactionStatus": "Paid", } processed = await service.process_wata_webhook(db, payload) assert processed is False