Spaces:
Running
Running
| """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 | |
| 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}") | |
| 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}") | |
| 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}") | |