"""Author RAG — Stripe Connect: Referral Payout Automation. When a referral commission reaches the $100 threshold, this service transfers the balance to the referrer's Stripe Connect account. Flow: 1. Referrer creates a Stripe Connect Express account (onboard flow) 2. We store their stripe_connect_account_id on their User record 3. When commission sum >= $100, we call stripe.Transfer.create() 4. Referrer receives funds in their bank account via Stripe Routes: POST /api/admin/{slug}/connect/onboard → get Stripe Connect onboarding URL GET /api/admin/{slug}/connect/status → check Connect account status POST /api/admin/{slug}/connect/payout → trigger manual payout (SuperAdmin) GET /api/admin/{slug}/connect/payouts → list past payouts """ import structlog from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select, func, update from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.dependencies import get_current_author, get_db from app.models.referral import ReferralCommission from app.models.user import User logger = structlog.get_logger(__name__) cfg = get_settings() router = APIRouter(prefix="/connect", tags=["Stripe Connect"]) PAYOUT_THRESHOLD_USD = 100.0 @router.post("/{slug}/onboard") async def onboard_connect_account( slug: str, db: AsyncSession = Depends(get_db), author=Depends(get_current_author), ): """Initiate Stripe Connect Express onboarding. Returns a URL the author visits to connect their bank account. Stripe redirects back to /dashboard after completion. """ if author.id != slug: raise HTTPException(403, "Forbidden") try: import stripe stripe.api_key = cfg.STRIPE_SECRET_KEY # Create Express account if not already connected connect_id = getattr(author, "stripe_connect_account_id", None) if not connect_id: account = stripe.Account.create( type="express", country="US", email=author.email, capabilities={"transfers": {"requested": True}}, business_profile={"mcc": "7372", "url": cfg.FRONTEND_URL}, ) connect_id = account.id await db.execute( update(User).where(User.id == slug) .values(stripe_connect_account_id=connect_id) ) await db.commit() # Generate onboarding link (expires in 5 mins) link = stripe.AccountLink.create( account=connect_id, refresh_url=f"{cfg.FRONTEND_URL}/dashboard?connect=refresh", return_url=f"{cfg.FRONTEND_URL}/dashboard?connect=success", type="account_onboarding", ) return {"onboarding_url": link.url, "connect_account_id": connect_id} except ImportError: raise HTTPException(503, "stripe package not installed") except Exception as e: raise HTTPException(502, f"Stripe Connect error: {e}") @router.get("/{slug}/status") async def connect_status( slug: str, db: AsyncSession = Depends(get_db), author=Depends(get_current_author), ): """Check Stripe Connect account status + pending payout balance.""" if author.id != slug: raise HTTPException(403, "Forbidden") connect_id = getattr(author, "stripe_connect_account_id", None) if not connect_id: return {"connected": False, "message": "No Stripe Connect account linked."} try: import stripe stripe.api_key = cfg.STRIPE_SECRET_KEY account = stripe.Account.retrieve(connect_id) # Pending commissions pending_r = await db.execute( select(func.sum(ReferralCommission.commission_amount_usd)) .where( ReferralCommission.referrer_id == slug, ReferralCommission.status == "approved", ) ) pending_usd = float(pending_r.scalar() or 0) return { "connected": True, "connect_account_id": connect_id, "payouts_enabled": account.payouts_enabled, "charges_enabled": account.charges_enabled, "pending_payout_usd": round(pending_usd, 2), "payout_ready": pending_usd >= PAYOUT_THRESHOLD_USD, "threshold_usd": PAYOUT_THRESHOLD_USD, } except Exception as e: raise HTTPException(502, f"Stripe error: {e}") @router.post("/{slug}/payout") async def trigger_payout( slug: str, db: AsyncSession = Depends(get_db), author=Depends(get_current_author), ): """Trigger referral commission payout via Stripe Transfer. Transfers all approved commissions to the author's Connect account. Requires payouts_enabled on their Connect account. """ if author.id != slug: raise HTTPException(403, "Forbidden") connect_id = getattr(author, "stripe_connect_account_id", None) if not connect_id: raise HTTPException(400, "No Stripe Connect account. Complete onboarding first.") # Sum approved commissions pending_r = await db.execute( select(ReferralCommission) .where( ReferralCommission.referrer_id == slug, ReferralCommission.status == "approved", ) ) commissions = list(pending_r.scalars()) total_usd = sum(c.commission_amount_usd for c in commissions) if total_usd < PAYOUT_THRESHOLD_USD: raise HTTPException( 400, f"Pending balance ${total_usd:.2f} is below ${PAYOUT_THRESHOLD_USD} threshold." ) try: import stripe stripe.api_key = cfg.STRIPE_SECRET_KEY # Transfer in cents amount_cents = int(total_usd * 100) transfer = stripe.Transfer.create( amount=amount_cents, currency="usd", destination=connect_id, description=f"Referral commission payout for {slug}", metadata={"author_id": slug, "commission_count": str(len(commissions))}, ) # Mark commissions as paid from datetime import datetime, timezone from sqlalchemy import update as sql_update ids = [c.id for c in commissions] await db.execute( sql_update(ReferralCommission) .where(ReferralCommission.id.in_(ids)) .values(status="paid", paid_at=datetime.now(timezone.utc)) ) await db.commit() logger.info("Referral payout completed", author=slug, amount=total_usd, transfer=transfer.id) return { "success": True, "transfer_id": transfer.id, "amount_usd": total_usd, "commissions_paid": len(commissions), } except Exception as e: logger.error("Payout failed", author=slug, error=str(e)) raise HTTPException(502, f"Transfer failed: {e}")