Arag / app /api /stripe_connect.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
6.92 kB
"""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}")