"""Integration tests for Creator Payouts: requests, list, admin review, and balance accounting. docs/planning/economy.md US-5 (tasks f5b4684f2e65, b52008923e93). """ import pytest from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.config import settings from app.main import app from app.models.marketplace import CreatorBalance from app.models.user import User def _register(client: TestClient, username: str) -> tuple[int, dict[str, str]]: resp = client.post( "/api/auth/register", json={ "username": username, "email": f"{username}@example.com", "password": "secret123", "date_of_birth": "1990-01-01", }, ) assert resp.status_code == 201, resp.text token = resp.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} me = client.get("/api/users/me", headers=headers) assert me.status_code == 200, me.text return me.json()["id"], headers async def _fund_creator_balance(user_id: int, available_amount: int): engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool) session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: cb = CreatorBalance( user_id=user_id, available=available_amount, pending=0, lifetime_earned=available_amount, ) session.add(cb) await session.commit() await engine.dispose() async def _make_admin(user_id: int): engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool) session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: user = await session.get(User, user_id) assert user is not None user.is_admin = True await session.commit() await engine.dispose() @pytest.mark.asyncio async def test_payout_request_and_validation(): client = TestClient(app) uid, headers = _register(client, "payout_creator_1") _, other_headers = _register(client, "payout_other_user") # Min threshold is 100 CR resp = client.post( "/api/payouts", headers=headers, json={"amount": 50}, ) assert resp.status_code == 422, resp.text # Without balance, payout of 200 CR fails resp = client.post( "/api/payouts", headers=headers, json={"amount": 200}, ) assert resp.status_code == 400 assert "Insufficient available balance" in resp.text # Seed 500 CR available creator balance await _fund_creator_balance(uid, 500) # Valid payout request of 200 CR resp = client.post( "/api/payouts", headers=headers, json={"amount": 200, "external_ref": "creator@paypal.com"}, ) assert resp.status_code == 201, resp.text payout_data = resp.json() assert payout_data["amount"] == 200 assert payout_data["status"] == "pending" assert payout_data["external_ref"] == "creator@paypal.com" payout_id = payout_data["id"] # Check creator balance: 500 - 200 = 300 available, 200 pending cb_resp = client.get("/api/marketplace/creator/balance", headers=headers) assert cb_resp.status_code == 200 cb = cb_resp.json() assert cb["available"] == 300 assert cb["pending"] == 200 # List payouts: creator sees own request list_resp = client.get("/api/payouts", headers=headers) assert list_resp.status_code == 200 my_payouts = list_resp.json() assert len(my_payouts) == 1 assert my_payouts[0]["id"] == payout_id # Other user sees empty list other_list = client.get("/api/payouts", headers=other_headers) assert other_list.status_code == 200 assert len(other_list.json()) == 0 # Other user cannot view detail -> 403 det_resp = client.get(f"/api/payouts/{payout_id}", headers=other_headers) assert det_resp.status_code == 403 @pytest.mark.asyncio async def test_payout_admin_approve_and_reject(): client = TestClient(app) admin_id, admin_headers = _register(client, "admin_payout_reviewer") creator_id, creator_headers = _register(client, "payout_creator_2") await _fund_creator_balance(creator_id, 1000) # 1. Request payout of 300 CR p1_resp = client.post( "/api/payouts", headers=creator_headers, json={"amount": 300, "external_ref": "iban_123"}, ) assert p1_resp.status_code == 201 p1_id = p1_resp.json()["id"] # 2. Non-admin cannot approve -> 403 non_admin_appr = client.post(f"/api/payouts/{p1_id}/approve", headers=creator_headers) assert non_admin_appr.status_code == 403 # 3. Admin approves payout 1 appr_resp = client.post(f"/api/payouts/{p1_id}/approve", headers=admin_headers) assert appr_resp.status_code == 200, appr_resp.text p1_data = appr_resp.json() assert p1_data["status"] == "paid" assert p1_data["reviewed_by"] == admin_id assert p1_data["provider_ref"] is not None # Balance check: available was 700, pending was 300 -> now pending = 0 cb_resp = client.get("/api/marketplace/creator/balance", headers=creator_headers) assert cb_resp.json()["available"] == 700 assert cb_resp.json()["pending"] == 0 # 4. Request payout 2 of 200 CR p2_resp = client.post( "/api/payouts", headers=creator_headers, json={"amount": 200}, ) assert p2_resp.status_code == 201 p2_id = p2_resp.json()["id"] # available was 700 - 200 = 500, pending = 200 cb_resp = client.get("/api/marketplace/creator/balance", headers=creator_headers) assert cb_resp.json()["available"] == 500 assert cb_resp.json()["pending"] == 200 # 5. Admin rejects payout 2 rej_resp = client.post( f"/api/payouts/{p2_id}/reject", headers=admin_headers, json={"reason": "Incorrect payment details"}, ) assert rej_resp.status_code == 200, rej_resp.text p2_data = rej_resp.json() assert p2_data["status"] == "rejected" assert p2_data["reviewed_by"] == admin_id # Creator balance refunded: 500 + 200 = 700 available, 0 pending cb_resp = client.get("/api/marketplace/creator/balance", headers=creator_headers) assert cb_resp.json()["available"] == 700 assert cb_resp.json()["pending"] == 0