"""Billing checkout integration tests (ticket 012). Uses respx to mock ЮKassa; enables YOOKASSA_ENABLED in the test env. """ from __future__ import annotations import uuid import httpx import pytest import respx from sqlalchemy import text from contract_check.core.db.session import create_session_factory from tests.integration.conftest import user_token pytestmark = pytest.mark.integration def _unique_tg() -> int: return abs(int(uuid.uuid4().int % 900000000000)) + 100000000000 async def _auth_env(client: httpx.AsyncClient, infra: dict[str, str]) -> tuple[int, str]: tg = _unique_tg() token = await user_token(client, infra, tg) return tg, token @pytest.fixture async def yookassa_enabled(): """Temporarily turn on YOOKASSA_ENABLED for tests.""" import os old = os.environ.get("YOOKASSA_ENABLED") os.environ["YOOKASSA_ENABLED"] = "true" os.environ["YOOKASSA_SHOP_ID"] = "123456" os.environ["YOOKASSA_SECRET_KEY"] = "test-secret" os.environ["YOOKASSA_RETURN_BASE_URL"] = "https://example.com/pay/{invoice_id}" os.environ["PRICE_PER_DOC_KOPECKS"] = "19900" yield if old is None: os.environ.pop("YOOKASSA_ENABLED", None) else: os.environ["YOOKASSA_ENABLED"] = old os.environ.pop("YOOKASSA_SHOP_ID", None) os.environ.pop("YOOKASSA_SECRET_KEY", None) os.environ.pop("YOOKASSA_RETURN_BASE_URL", None) @respx.mock async def test_checkout_topup_creates_invoice( client: httpx.AsyncClient, infra: dict[str, str], yookassa_enabled, ) -> None: get_settings.cache_clear() _, token = await _auth_env(client, infra) respx.post("https://api.yookassa.ru/v3/payments").mock( return_value=httpx.Response( 200, json={ "id": "pay-topup-1", "status": "pending", "confirmation": {"confirmation_url": "https://yoo.test/topup-1"}, }, ) ) r = await client.post( "/api/v1/billing/checkout", json={"kind": "topup", "credits": 5}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 201, r.text body = r.json() assert body["invoice_id"] assert body["confirmation_url"] == "https://yoo.test/topup-1" # Invoice persisted as pending. factory = create_session_factory() async with factory() as s: row = ( await s.execute( text( "SELECT amount, kind, credits_purchased, status, external_id, confirmation_url FROM invoices WHERE id = :id" ), {"id": uuid.UUID(body["invoice_id"])}, ) ).first() assert row is not None assert int(row[0]) == 5 * 19900 assert row[1] == "topup" assert row[2] == 5 assert row[3] == "pending" assert row[4] == "pay-topup-1" assert row[5] == "https://yoo.test/topup-1" async def test_checkout_payments_disabled_503( client: httpx.AsyncClient, infra: dict[str, str] ) -> None: _, token = await _auth_env(client, infra) r = await client.post( "/api/v1/billing/checkout", json={"kind": "topup", "credits": 1}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 503 async def test_checkout_invalid_pack_422( client: httpx.AsyncClient, infra: dict[str, str], yookassa_enabled, ) -> None: get_settings.cache_clear() _, token = await _auth_env(client, infra) r = await client.post( "/api/v1/billing/checkout", json={"kind": "topup", "credits": 7}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 422 async def test_plans_catalog_visible_when_payments_off( client: httpx.AsyncClient, ) -> None: r = await client.get("/api/v1/billing/plans") assert r.status_code == 200 body = r.json() assert len(body["plans"]) == 4 assert body["price_per_doc_kopecks"] == 19900 async def test_invoices_requires_auth(client: httpx.AsyncClient) -> None: r = await client.get("/api/v1/billing/invoices") assert r.status_code == 401 # Import needed inside fixture to avoid circular import issues at collection time. from contract_check.core.config import get_settings # noqa: E402