""" Admin endpoints for Yield payouts (ledger). Premium constraints: - No placeholder payouts - No currency mixing - Idempotent generation per (user, currency, period) """ from __future__ import annotations from datetime import datetime from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy import and_, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db from app.models.user import User from app.models.yield_domain import YieldPayout, YieldTransaction from app.services.telemetry import track_event from app.services.yield_payouts import generate_payouts_for_period router = APIRouter(prefix="/yield", tags=["yield-admin"]) class PayoutGenerateRequest(BaseModel): period_start: datetime period_end: datetime class GeneratedPayout(BaseModel): id: int user_id: int amount: Decimal currency: str period_start: datetime period_end: datetime transaction_count: int status: str created_at: datetime class PayoutGenerateResponse(BaseModel): created: list[GeneratedPayout] skipped_existing: int = 0 class PayoutCompleteRequest(BaseModel): payment_method: str | None = Field(default=None, max_length=50) payment_reference: str | None = Field(default=None, max_length=200) class PayoutCompleteResponse(BaseModel): payout_id: int transactions_marked_paid: int completed_at: datetime def _require_admin(current_user: User) -> None: if not current_user.is_admin: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") @router.post("/payouts/generate", response_model=PayoutGenerateResponse) async def generate_payouts( payload: PayoutGenerateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Create YieldPayout rows for confirmed, unpaid transactions in the period. This does NOT mark payouts as completed. It only assigns transactions to a payout via payout_id. Completion is a separate step once payment is executed. """ _require_admin(current_user) if payload.period_end <= payload.period_start: raise HTTPException(status_code=400, detail="period_end must be after period_start") created_count, skipped_existing = await generate_payouts_for_period( db, period_start=payload.period_start, period_end=payload.period_end, ) payouts = ( await db.execute( select(YieldPayout) .where( and_( YieldPayout.period_start == payload.period_start, YieldPayout.period_end == payload.period_end, ) ) .order_by(YieldPayout.created_at.desc()) ) ).scalars().all() created = [ GeneratedPayout( id=p.id, user_id=p.user_id, amount=p.amount, currency=p.currency, period_start=p.period_start, period_end=p.period_end, transaction_count=p.transaction_count, status=p.status, created_at=p.created_at, ) for p in payouts ] # created_count is still returned implicitly via list length; we keep it for logs later _ = created_count return PayoutGenerateResponse(created=created, skipped_existing=skipped_existing) @router.post("/payouts/{payout_id}/complete", response_model=PayoutCompleteResponse) async def complete_payout( payout_id: int, payload: PayoutCompleteRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Mark a payout as completed and mark assigned transactions as paid. """ _require_admin(current_user) payout = ( await db.execute(select(YieldPayout).where(YieldPayout.id == payout_id)) ).scalar_one_or_none() if not payout: raise HTTPException(status_code=404, detail="Payout not found") if payout.status == "completed": raise HTTPException(status_code=400, detail="Payout already completed") payout.status = "completed" payout.completed_at = datetime.utcnow() payout.payment_method = payload.payment_method payout.payment_reference = payload.payment_reference txs = ( await db.execute( select(YieldTransaction).where(YieldTransaction.payout_id == payout.id) ) ).scalars().all() marked = 0 for tx in txs: if tx.status != "paid": tx.status = "paid" tx.paid_at = payout.completed_at marked += 1 await track_event( db, event_name="payout_paid", request=None, user_id=payout.user_id, is_authenticated=None, source="admin", domain=None, yield_domain_id=None, metadata={ "payout_id": payout.id, "currency": payout.currency, "amount": float(payout.amount), "transaction_count": payout.transaction_count, "payment_method": payout.payment_method, }, ) await db.commit() return PayoutCompleteResponse( payout_id=payout.id, transactions_marked_paid=marked, completed_at=payout.completed_at, )