system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/guarantees.py
Claude 84762f08ab
Add complete launch infrastructure: models, APIs, agents, compliance, docs, knowledge base
Phase 1 - Repo Hardening:
- README.md, LICENSE, SECURITY.md, CONTRIBUTING.md
- GitHub Actions repo-hygiene workflow
- docs/: ARCHITECTURE, DATA-MODEL, API-MAP, AGENT-MAP, DEPLOYMENT-NOTES

Phase 2 - Database Models (7 new):
- Company, Contact, Call, Commission, Payout, Dispute, GuaranteeClaim
- Consent, Complaint, Policy, KnowledgeArticle, SectorAsset
- Updated models/__init__.py with all 32+ models

Phase 3 - API Surfaces (16 new route files):
- companies, contacts, calls, meetings, commissions, payouts
- disputes, guarantees, consents, complaints, knowledge
- sectors, presentations, supervisor, admin, health
- Updated router.py with all 24 route groups

Phase 4 - AI Prompt Registry (18 agent contracts):
- Lead Qualification, Affiliate Recruitment Evaluator, Onboarding Coach
- Outreach Writer, Arabic WhatsApp, English Conversation, Voice Call
- Meeting Booking, Sector Strategist, Objection Handler
- Proposal Drafter, QA Reviewer, Compliance Reviewer
- Knowledge Retrieval, Revenue Attribution, Fraud Reviewer
- Guarantee Claim Reviewer, Management Summary

Phase 5 - Communication Templates:
- 15 production templates (WhatsApp, email, voice, internal)
- Arabic + English variants with variable interpolation

Phase 6 - Compliance Center (7 legal docs):
- Privacy policy, Terms of service, Refund policy
- Commission policy, Affiliate rules, Consent policy, Data protection
- All PDPL-compliant, Arabic

Phase 7 - Celery Workers (fully implemented):
- follow_up_tasks: automated lead follow-ups with workflow execution
- message_tasks: WhatsApp/email/SMS with retry logic
- notification_tasks: daily reports, meeting reminders, in-app notifications
- affiliate_tasks: target checking, commission calculation, weekly reports, AI outreach

Phase 8 - Knowledge Base OS (8 files):
- Services overview, Pricing policy, Channel policy, Meeting policy
- Identity rules, Escalation rules, Hiring path, Internal SOPs

https://claude.ai/code/session_01KnJgK7RwyeCvRZTRThHtfU
2026-03-31 07:57:48 +00:00

259 lines
9.0 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from uuid import UUID
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel as Schema
from app.database import get_db
from app.api.deps import get_current_user, require_role
from app.models.user import User
from app.models.guarantee import GuaranteeClaim, GuaranteeStatus
router = APIRouter()
class GuaranteeCreate(Schema):
customer_id: UUID
deal_id: UUID
subscription_id: Optional[UUID] = None
reason: str
evidence: Optional[dict] = None
leads_entered: int = 0
messages_sent: int = 0
active_days: int = 0
onboarding_completed: bool = False
class GuaranteeUpdate(Schema):
reason: Optional[str] = None
evidence: Optional[dict] = None
leads_entered: Optional[int] = None
messages_sent: Optional[int] = None
active_days: Optional[int] = None
onboarding_completed: Optional[bool] = None
class GuaranteeResponse(Schema):
id: UUID
tenant_id: UUID
customer_id: UUID
deal_id: UUID
subscription_id: Optional[UUID] = None
status: str
reason: str
evidence: Optional[dict] = None
leads_entered: int
messages_sent: int
active_days: int
onboarding_completed: bool
reviewer_id: Optional[UUID] = None
reviewed_at: Optional[datetime] = None
decision_notes: Optional[str] = None
refund_amount: Optional[float] = None
refunded_at: Optional[datetime] = None
created_at: datetime
model_config = {"from_attributes": True}
class GuaranteeListResponse(Schema):
items: list[GuaranteeResponse]
total: int
page: int
per_page: int
class ReviewDecision(Schema):
decision_notes: Optional[str] = None
class RefundRequest(Schema):
refund_amount: float
@router.get("", response_model=GuaranteeListResponse)
async def list_guarantees(
customer_id: UUID = Query(None),
status: str = Query(None),
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
query = select(GuaranteeClaim).where(GuaranteeClaim.tenant_id == current_user.tenant_id)
if customer_id:
query = query.where(GuaranteeClaim.customer_id == customer_id)
if status:
query = query.where(GuaranteeClaim.status == status)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
query = query.order_by(GuaranteeClaim.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
result = await db.execute(query)
items = [GuaranteeResponse.model_validate(g) for g in result.scalars().all()]
return GuaranteeListResponse(items=items, total=total, page=page, per_page=per_page)
@router.get("/{claim_id}", response_model=GuaranteeResponse)
async def get_guarantee(
claim_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
return GuaranteeResponse.model_validate(claim)
@router.post("", response_model=GuaranteeResponse, status_code=201)
async def create_guarantee(
data: GuaranteeCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
claim = GuaranteeClaim(
tenant_id=current_user.tenant_id,
**data.model_dump(exclude_none=True),
)
db.add(claim)
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.put("/{claim_id}", response_model=GuaranteeResponse)
async def update_guarantee(
claim_id: UUID,
data: GuaranteeUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status != GuaranteeStatus.SUBMITTED:
raise HTTPException(status_code=400, detail="Can only update submitted claims")
for field, value in data.model_dump(exclude_none=True).items():
setattr(claim, field, value)
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.post("/{claim_id}/review", response_model=GuaranteeResponse)
async def review_guarantee(
claim_id: UUID,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status != GuaranteeStatus.SUBMITTED:
raise HTTPException(status_code=400, detail="Claim is not in submitted status")
claim.status = GuaranteeStatus.REVIEWING
claim.reviewer_id = current_user.id
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.post("/{claim_id}/approve", response_model=GuaranteeResponse)
async def approve_guarantee(
claim_id: UUID,
data: ReviewDecision,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status not in (GuaranteeStatus.SUBMITTED, GuaranteeStatus.REVIEWING):
raise HTTPException(status_code=400, detail="Claim cannot be approved in current status")
claim.status = GuaranteeStatus.APPROVED
claim.reviewer_id = current_user.id
claim.reviewed_at = datetime.now(timezone.utc)
claim.decision_notes = data.decision_notes
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.post("/{claim_id}/reject", response_model=GuaranteeResponse)
async def reject_guarantee(
claim_id: UUID,
data: ReviewDecision,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status not in (GuaranteeStatus.SUBMITTED, GuaranteeStatus.REVIEWING):
raise HTTPException(status_code=400, detail="Claim cannot be rejected in current status")
claim.status = GuaranteeStatus.REJECTED
claim.reviewer_id = current_user.id
claim.reviewed_at = datetime.now(timezone.utc)
claim.decision_notes = data.decision_notes
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.post("/{claim_id}/refund", response_model=GuaranteeResponse)
async def refund_guarantee(
claim_id: UUID,
data: RefundRequest,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status != GuaranteeStatus.APPROVED:
raise HTTPException(status_code=400, detail="Claim must be approved before refund")
claim.status = GuaranteeStatus.REFUNDED
claim.refund_amount = data.refund_amount
claim.refunded_at = datetime.now(timezone.utc)
await db.flush()
await db.refresh(claim)
return GuaranteeResponse.model_validate(claim)
@router.delete("/{claim_id}", status_code=204)
async def delete_guarantee(
claim_id: UUID,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(GuaranteeClaim).where(GuaranteeClaim.id == claim_id, GuaranteeClaim.tenant_id == current_user.tenant_id)
)
claim = result.scalar_one_or_none()
if not claim:
raise HTTPException(status_code=404, detail="Guarantee claim not found")
if claim.status != GuaranteeStatus.SUBMITTED:
raise HTTPException(status_code=400, detail="Can only delete submitted claims")
await db.delete(claim)
await db.flush()