system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/affiliates.py
2026-04-04 18:04:21 +03:00

232 lines
8.2 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional
from datetime import datetime, timezone
from pydantic import BaseModel, ConfigDict
from uuid import UUID
import uuid
from app.database import get_db
from app.models.affiliate import AffiliateMarketer, AffiliatePerformance, AffiliateDeal, AffiliateStatus
router = APIRouter(prefix="/affiliates", tags=["affiliates"])
# ─── Schemas ─────────────────────────────────────────────
class AffiliateRegisterRequest(BaseModel):
full_name: str
full_name_ar: Optional[str] = None
email: str
phone: str
whatsapp: Optional[str] = None
city: Optional[str] = None
national_id: Optional[str] = None
class AffiliateResponse(BaseModel):
id: UUID
full_name: str
email: str
phone: str
status: str
referral_code: str
total_deals_closed: int
total_commission_earned: float
current_month_deals: int
model_config = ConfigDict(from_attributes=True)
class AffiliateDealRequest(BaseModel):
client_company: str
client_contact: Optional[str] = None
client_phone: Optional[str] = None
client_email: Optional[str] = None
plan_type: str # basic, professional, enterprise
class AffiliatePerformanceResponse(BaseModel):
month: str
leads_generated: int
calls_made: int
meetings_booked: int
deals_closed: int
commission_earned: float
bonus_earned: float
payment_status: str
# ─── Commission Rates ────────────────────────────────────
COMMISSION_RATES = {
"basic": {"price": 299, "rate": 0.15},
"professional": {"price": 699, "rate": 0.20},
"enterprise": {"price": 1499, "rate": 0.25},
}
BONUS_TIERS = [
{"min_deals": 5, "bonus": 500},
{"min_deals": 10, "bonus": 1500},
{"min_deals": 15, "bonus": 3000},
]
def generate_referral_code() -> str:
return f"DLX-{uuid.uuid4().hex[:8].upper()}"
# ─── Endpoints ───────────────────────────────────────────
@router.post("/register", response_model=AffiliateResponse, status_code=status.HTTP_201_CREATED)
async def register_affiliate(data: AffiliateRegisterRequest, db: AsyncSession = Depends(get_db)):
"""Register a new affiliate marketer."""
existing = await db.execute(
select(AffiliateMarketer).where(AffiliateMarketer.email == data.email)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
affiliate = AffiliateMarketer(
full_name=data.full_name,
full_name_ar=data.full_name_ar,
email=data.email,
phone=data.phone,
whatsapp=data.whatsapp or data.phone,
city=data.city,
national_id=data.national_id,
status=AffiliateStatus.PENDING,
referral_code=generate_referral_code(),
)
db.add(affiliate)
await db.commit()
await db.refresh(affiliate)
return affiliate
@router.get("/program")
async def affiliate_program_public():
"""رحلة المسوق + شرائح العمولة — للواجهة والتسويق (بدون DB)."""
return {
"title_ar": "برنامج الشراكة Dealix",
"journey_ar": [
{"step": 1, "title": "التسجيل", "detail_ar": "بياناتك ورمز إحالة فريد خلال دقائق."},
{"step": 2, "title": "التفعيل", "detail_ar": "موافقة فريقنا ثم تفعيل الحساب وربط القنوات."},
{"step": 3, "title": "أول عميل", "detail_ar": "مشاركة الرابط أو تسجيل صفقة من اللوحة."},
{"step": 4, "title": "تتبع العمولة", "detail_ar": "شفافية من الصفقة حتى الاعتماد والدفع."},
{"step": 5, "title": "نمو وترقية", "detail_ar": "مكافآت إضافية ومسار توظيف عند الأداء العالي."},
],
"commission_rates": COMMISSION_RATES,
"bonus_tiers": BONUS_TIERS,
"auto_employ_rule_ar": "عند 10 صفقات مُسجّلة في الشهر والحالة نشط — يُقيَّد كمرشح توظيف تلقائي.",
}
@router.get("/leaderboard/top")
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
"""Get top performing affiliates."""
result = await db.execute(
select(AffiliateMarketer)
.where(AffiliateMarketer.status.in_([AffiliateStatus.ACTIVE, AffiliateStatus.EMPLOYED]))
.order_by(AffiliateMarketer.total_deals_closed.desc())
.limit(limit)
)
affiliates = result.scalars().all()
return [
{
"name": a.full_name_ar or a.full_name,
"deals": a.total_deals_closed,
"commission": a.total_commission_earned,
"status": a.status.value,
}
for a in affiliates
]
@router.get("/{affiliate_id}", response_model=AffiliateResponse)
async def get_affiliate(affiliate_id: UUID, db: AsyncSession = Depends(get_db)):
"""Get affiliate details."""
result = await db.execute(
select(AffiliateMarketer).where(AffiliateMarketer.id == affiliate_id)
)
affiliate = result.scalar_one_or_none()
if not affiliate:
raise HTTPException(status_code=404, detail="Affiliate not found")
return affiliate
@router.post("/{affiliate_id}/activate", response_model=AffiliateResponse)
async def activate_affiliate(affiliate_id: UUID, db: AsyncSession = Depends(get_db)):
"""Activate an affiliate after onboarding."""
result = await db.execute(
select(AffiliateMarketer).where(AffiliateMarketer.id == affiliate_id)
)
affiliate = result.scalar_one_or_none()
if not affiliate:
raise HTTPException(status_code=404, detail="Affiliate not found")
affiliate.status = AffiliateStatus.ACTIVE
affiliate.onboarded_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(affiliate)
return affiliate
@router.post("/{affiliate_id}/deals", status_code=status.HTTP_201_CREATED)
async def submit_deal(
affiliate_id: UUID,
data: AffiliateDealRequest,
db: AsyncSession = Depends(get_db),
):
"""Submit a new deal for commission tracking."""
result = await db.execute(
select(AffiliateMarketer).where(AffiliateMarketer.id == affiliate_id)
)
affiliate = result.scalar_one_or_none()
if not affiliate:
raise HTTPException(status_code=404, detail="Affiliate not found")
if data.plan_type not in COMMISSION_RATES:
raise HTTPException(status_code=400, detail="Invalid plan type")
rate_info = COMMISSION_RATES[data.plan_type]
commission = rate_info["price"] * rate_info["rate"]
deal = AffiliateDeal(
affiliate_id=affiliate_id,
client_company=data.client_company,
client_contact=data.client_contact,
client_phone=data.client_phone,
client_email=data.client_email,
plan_type=data.plan_type,
plan_price=rate_info["price"],
commission_rate=rate_info["rate"],
commission_amount=commission,
)
db.add(deal)
# Update affiliate counters
affiliate.total_deals_closed += 1
affiliate.current_month_deals += 1
affiliate.total_commission_earned += commission
# Auto-employ if target reached (10 deals/month)
if affiliate.current_month_deals >= 10 and affiliate.status == AffiliateStatus.ACTIVE:
affiliate.status = AffiliateStatus.EMPLOYED
affiliate.employed_at = datetime.now(timezone.utc)
await db.commit()
return {"message": "Deal submitted successfully", "commission": commission}
@router.get("/{affiliate_id}/performance", response_model=list[AffiliatePerformanceResponse])
async def get_performance(affiliate_id: UUID, db: AsyncSession = Depends(get_db)):
"""Get affiliate performance history."""
result = await db.execute(
select(AffiliatePerformance)
.where(AffiliatePerformance.affiliate_id == affiliate_id)
.order_by(AffiliatePerformance.month.desc())
)
return result.scalars().all()