Arag / app /api /referral.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
6.05 kB
"""Author RAG — Referral System API.
Routes:
POST /api/admin/{slug}/referral/generate → create referral link + Stripe coupon
GET /api/admin/{slug}/referral/stats → clicks, signups, pending commissions
GET /api/admin/{slug}/referral/commissions → detailed commission list
"""
import structlog
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_author, get_db
from app.models.referral import ReferralLink, ReferralCommission
from app.config import get_settings
logger = structlog.get_logger(__name__)
cfg = get_settings()
router = APIRouter()
def _gen_code(author_name: str) -> str:
"""Generate a human-readable referral code from author name."""
import re, random, string
slug = re.sub(r"[^a-z0-9]", "", author_name.lower())[:10]
suffix = "".join(random.choices(string.digits, k=4))
return f"{slug}{suffix}" if slug else f"ref{suffix}"
@router.post("/{slug}/referral/generate", status_code=201)
async def generate_referral_link(
slug: str,
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""Generate (or return existing) referral link for this author.
Creates a Stripe coupon that gives referred customers 20% off for 3 months.
Author earns 20% commission on those payments.
"""
if author.id != slug:
raise HTTPException(403, "Forbidden")
# Return existing active link if already generated
existing = await db.execute(
select(ReferralLink).where(
ReferralLink.referrer_id == slug,
ReferralLink.is_active == True, # noqa: E712
)
)
link = existing.scalar_one_or_none()
if link:
return _link_dict(link)
# Generate new code
code = _gen_code(author.full_name or "author")
# Create Stripe coupon (20% off for 3 months)
stripe_coupon_id = None
try:
import stripe
stripe.api_key = cfg.STRIPE_SECRET_KEY
if cfg.STRIPE_SECRET_KEY:
coupon = stripe.Coupon.create(
id=f"{code}_DISC",
percent_off=20,
duration="repeating",
duration_in_months=3,
)
stripe_coupon_id = coupon.id
except Exception as e:
logger.warning("Stripe coupon creation failed — link created without coupon", error=str(e))
link = ReferralLink(
referrer_id=slug,
code=code,
stripe_coupon_id=stripe_coupon_id,
discount_pct=20,
discount_months=3,
)
db.add(link)
await db.commit()
logger.info("Referral link generated", author_id=slug, code=code)
return _link_dict(link)
@router.get("/{slug}/referral/stats")
async def referral_stats(
slug: str,
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""Return referral program stats — clicks, signups, earnings."""
if author.id != slug:
raise HTTPException(403, "Forbidden")
link_result = await db.execute(
select(ReferralLink).where(ReferralLink.referrer_id == slug, ReferralLink.is_active == True) # noqa: E712
)
link = link_result.scalar_one_or_none()
if not link:
return {"has_link": False, "message": "Generate your referral link to start earning."}
# Commission totals
comm_result = await db.execute(
select(
func.count(ReferralCommission.id),
func.sum(ReferralCommission.commission_amount_usd),
).where(ReferralCommission.referrer_id == slug)
)
total_count, total_earned = comm_result.one()
total_earned = float(total_earned or 0)
pending_result = await db.execute(
select(func.sum(ReferralCommission.commission_amount_usd))
.where(ReferralCommission.referrer_id == slug, ReferralCommission.status == "pending")
)
pending = float(pending_result.scalar() or 0)
return {
"has_link": True,
"code": link.code,
"referral_url": f"{cfg.FRONTEND_URL}/?ref={link.code}",
"clicks": link.click_count,
"signups": link.signup_count,
"total_commissions": total_count or 0,
"total_earned_usd": round(total_earned, 2),
"pending_payout_usd": round(pending, 2),
"payout_threshold_usd": 100.0,
"discount_offer": f"{link.discount_pct}% off for {link.discount_months} months (for new customers)",
}
@router.get("/{slug}/referral/commissions")
async def list_commissions(
slug: str,
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""Detailed list of commission records."""
if author.id != slug:
raise HTTPException(403, "Forbidden")
result = await db.execute(
select(ReferralCommission)
.where(ReferralCommission.referrer_id == slug)
.order_by(ReferralCommission.created_at.desc())
.limit(100)
)
commissions = result.scalars().all()
return {
"commissions": [
{
"id": c.id,
"payment_amount_usd": c.payment_amount_usd,
"commission_amount_usd": c.commission_amount_usd,
"status": c.status,
"created_at": c.created_at.isoformat() if c.created_at else None,
"paid_at": c.paid_at.isoformat() if c.paid_at else None,
}
for c in commissions
]
}
def _link_dict(link: ReferralLink) -> dict:
return {
"id": link.id,
"code": link.code,
"referral_url": f"{cfg.FRONTEND_URL}/?ref={link.code}",
"stripe_coupon_id": link.stripe_coupon_id,
"discount_pct": link.discount_pct,
"discount_months": link.discount_months,
"clicks": link.click_count,
"signups": link.signup_count,
"is_active": link.is_active,
"created_at": link.created_at.isoformat() if link.created_at else None,
}