109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""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()
|