"""Integration tests for Payout Admin Review, PayoutProvider, and Ledger Audit Trail. docs/planning/economy.md US-5 (task b52008923e93). """ import pytest from fastapi.testclient import TestClient from sqlalchemy import select 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.wallet import Transaction, TransactionType from app.services.payout import NoopPayoutProvider 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() @pytest.mark.asyncio async def test_payout_provider_noop(): provider = NoopPayoutProvider() success, ref = await provider.pay(user_id=1, amount=500, external_ref="test@example.com") assert success is True assert ref.startswith("noop_payout_") @pytest.mark.asyncio async def test_payout_admin_review_audit_trail_and_ledger(): client = TestClient(app) admin_id, admin_headers = _register(client, "admin_audit_reviewer") creator_id, creator_headers = _register(client, "audit_creator_user") await _fund_creator_balance(creator_id, 1500) # 1. Creator requests payout req_resp = client.post( "/api/payouts", headers=creator_headers, json={"amount": 600, "external_ref": "stripe_acct_456"}, ) assert req_resp.status_code == 201, req_resp.text payout_id = req_resp.json()["id"] # 2. Admin approves payout appr_resp = client.post( f"/api/payouts/{payout_id}/approve", headers=admin_headers, ) assert appr_resp.status_code == 200, appr_resp.text data = appr_resp.json() assert data["status"] == "paid" assert data["reviewed_by"] == admin_id assert data["reviewed_at"] is not None assert data["provider_ref"] is not None provider_ref = data["provider_ref"] # 3. Verify ledger transaction row was recorded for audit trail engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool) session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: stmt = ( select(Transaction) .where( Transaction.reference_type == "payout_request", Transaction.reference_id == str(payout_id), ) ) tx = (await session.execute(stmt)).scalar_one_or_none() assert tx is not None assert tx.type == TransactionType.payout assert tx.amount == -600 assert tx.metadata_json["provider_ref"] == provider_ref assert tx.metadata_json["reviewed_by"] == admin_id assert tx.metadata_json["external_ref"] == "stripe_acct_456" await engine.dispose()