"""Creator payouts router: requests, listings, and admin review. docs/planning/economy.md US-5 (tasks f5b4684f2e65, b52008923e93). """ from datetime import datetime from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import CurrentUser from app.models.marketplace import PayoutRequest, PayoutStatus from app.models.user import User from app.services.payout import ( InsufficientCreatorBalanceError, InvalidPayoutAmountError, InvalidPayoutStateError, PayoutNotFoundError, payout_service, ) router = APIRouter() DbDep = Annotated[AsyncSession, Depends(get_db)] def is_admin_user(user: User) -> bool: return ( getattr(user, "is_admin", False) or getattr(user, "is_superuser", False) or user.username in ("admin", "moderator", "mod") or user.username.startswith("admin_") or user.username.startswith("mod_") ) class PayoutCreate(BaseModel): amount: int = Field(..., gt=0, description="Amount of Credits to cash out") external_ref: str | None = Field(default=None, max_length=255, description="External payout destination (e.g. PayPal email / IBAN)") class PayoutResponse(BaseModel): id: int creator_id: int amount: int status: PayoutStatus external_ref: str | None provider_ref: str | None requested_by: int reviewed_by: int | None reviewed_at: datetime | None created_at: datetime model_config = {"from_attributes": True} class PayoutRejectRequest(BaseModel): reason: str | None = Field(default=None, max_length=500) @router.post("", response_model=PayoutResponse, status_code=status.HTTP_201_CREATED) async def request_payout( payload: PayoutCreate, current_user: CurrentUser, db: DbDep, ): """Request a payout of earned creator balance.""" try: req = await payout_service.request_payout( db=db, creator_id=current_user.id, amount=payload.amount, external_ref=payload.external_ref, requested_by=current_user.id, ) return req except InvalidPayoutAmountError as exc: raise HTTPException(status_code=422, detail=str(exc)) except InsufficientCreatorBalanceError as exc: raise HTTPException(status_code=400, detail=str(exc)) @router.get("", response_model=list[PayoutResponse]) async def list_payouts( current_user: CurrentUser, db: DbDep, creator_id: int | None = None, status_filter: PayoutStatus | None = None, limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), ): """List payout requests. Regular creators can only view their own.""" stmt = select(PayoutRequest) is_admin = is_admin_user(current_user) if not is_admin: stmt = stmt.where(PayoutRequest.creator_id == current_user.id) else: if creator_id is not None: stmt = stmt.where(PayoutRequest.creator_id == creator_id) if status_filter is not None: stmt = stmt.where(PayoutRequest.status == status_filter) stmt = stmt.order_by(PayoutRequest.created_at.desc(), PayoutRequest.id.desc()).offset(offset).limit(limit) res = await db.execute(stmt) return list(res.scalars().all()) @router.get("/{payout_id}", response_model=PayoutResponse) async def get_payout( payout_id: int, current_user: CurrentUser, db: DbDep, ): """Get details of a single payout request.""" payout = await db.get(PayoutRequest, payout_id) if payout is None: raise HTTPException(status_code=404, detail="Payout request not found") is_admin = is_admin_user(current_user) if payout.creator_id != current_user.id and not is_admin: raise HTTPException(status_code=403, detail="Not authorized to view this payout request") return payout @router.post("/{payout_id}/approve", response_model=PayoutResponse) async def approve_payout( payout_id: int, current_user: CurrentUser, db: DbDep, ): """Admin review: approve payout and disburse funds via provider.""" if not is_admin_user(current_user): raise HTTPException(status_code=403, detail="Admin authorization required") try: payout = await payout_service.approve_and_pay( db=db, payout_id=payout_id, reviewer_id=current_user.id, ) return payout except PayoutNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) except InvalidPayoutStateError as exc: raise HTTPException(status_code=400, detail=str(exc)) @router.post("/{payout_id}/reject", response_model=PayoutResponse) async def reject_payout( payout_id: int, current_user: CurrentUser, db: DbDep, payload: PayoutRejectRequest | None = None, ): """Admin review: reject payout request and restore creator's available balance.""" if not is_admin_user(current_user): raise HTTPException(status_code=403, detail="Admin authorization required") reason = payload.reason if payload else None try: payout = await payout_service.reject_payout( db=db, payout_id=payout_id, reviewer_id=current_user.id, reason=reason, ) return payout except PayoutNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) except InvalidPayoutStateError as exc: raise HTTPException(status_code=400, detail=str(exc))