system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/disputes.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

209 lines
6.9 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.dispute import Dispute, DisputeStatus
router = APIRouter()
class DisputeCreate(Schema):
affiliate_id: UUID
commission_id: Optional[UUID] = None
deal_id: Optional[UUID] = None
type: str
subject: str
description: Optional[str] = None
evidence: Optional[dict] = None
class DisputeUpdate(Schema):
subject: Optional[str] = None
description: Optional[str] = None
evidence: Optional[dict] = None
class DisputeResponse(Schema):
id: UUID
tenant_id: UUID
commission_id: Optional[UUID] = None
deal_id: Optional[UUID] = None
affiliate_id: UUID
type: str
status: str
subject: str
description: Optional[str] = None
evidence: Optional[dict] = None
resolution: Optional[str] = None
resolved_by: Optional[UUID] = None
resolved_at: Optional[datetime] = None
escalated_to: Optional[UUID] = None
created_at: datetime
model_config = {"from_attributes": True}
class DisputeListResponse(Schema):
items: list[DisputeResponse]
total: int
page: int
per_page: int
class ResolveRequest(Schema):
resolution: str
class EscalateRequest(Schema):
escalate_to: UUID
@router.get("", response_model=DisputeListResponse)
async def list_disputes(
affiliate_id: UUID = Query(None),
status: str = Query(None),
type: 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(Dispute).where(Dispute.tenant_id == current_user.tenant_id)
if affiliate_id:
query = query.where(Dispute.affiliate_id == affiliate_id)
if status:
query = query.where(Dispute.status == status)
if type:
query = query.where(Dispute.type == type)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
query = query.order_by(Dispute.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
result = await db.execute(query)
items = [DisputeResponse.model_validate(d) for d in result.scalars().all()]
return DisputeListResponse(items=items, total=total, page=page, per_page=per_page)
@router.get("/{dispute_id}", response_model=DisputeResponse)
async def get_dispute(
dispute_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Dispute).where(Dispute.id == dispute_id, Dispute.tenant_id == current_user.tenant_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise HTTPException(status_code=404, detail="Dispute not found")
return DisputeResponse.model_validate(dispute)
@router.post("", response_model=DisputeResponse, status_code=201)
async def create_dispute(
data: DisputeCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
dispute = Dispute(
tenant_id=current_user.tenant_id,
status=DisputeStatus.OPEN,
**data.model_dump(exclude_none=True),
)
db.add(dispute)
await db.flush()
await db.refresh(dispute)
return DisputeResponse.model_validate(dispute)
@router.put("/{dispute_id}", response_model=DisputeResponse)
async def update_dispute(
dispute_id: UUID,
data: DisputeUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Dispute).where(Dispute.id == dispute_id, Dispute.tenant_id == current_user.tenant_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise HTTPException(status_code=404, detail="Dispute not found")
if dispute.status in (DisputeStatus.RESOLVED, DisputeStatus.REJECTED):
raise HTTPException(status_code=400, detail="Cannot update a closed dispute")
for field, value in data.model_dump(exclude_none=True).items():
setattr(dispute, field, value)
await db.flush()
await db.refresh(dispute)
return DisputeResponse.model_validate(dispute)
@router.post("/{dispute_id}/resolve", response_model=DisputeResponse)
async def resolve_dispute(
dispute_id: UUID,
data: ResolveRequest,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Dispute).where(Dispute.id == dispute_id, Dispute.tenant_id == current_user.tenant_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise HTTPException(status_code=404, detail="Dispute not found")
if dispute.status in (DisputeStatus.RESOLVED, DisputeStatus.REJECTED):
raise HTTPException(status_code=400, detail="Dispute is already closed")
dispute.status = DisputeStatus.RESOLVED
dispute.resolution = data.resolution
dispute.resolved_by = current_user.id
dispute.resolved_at = datetime.now(timezone.utc)
await db.flush()
await db.refresh(dispute)
return DisputeResponse.model_validate(dispute)
@router.post("/{dispute_id}/escalate", response_model=DisputeResponse)
async def escalate_dispute(
dispute_id: UUID,
data: EscalateRequest,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Dispute).where(Dispute.id == dispute_id, Dispute.tenant_id == current_user.tenant_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise HTTPException(status_code=404, detail="Dispute not found")
if dispute.status in (DisputeStatus.RESOLVED, DisputeStatus.REJECTED):
raise HTTPException(status_code=400, detail="Cannot escalate a closed dispute")
dispute.status = DisputeStatus.ESCALATED
dispute.escalated_to = data.escalate_to
await db.flush()
await db.refresh(dispute)
return DisputeResponse.model_validate(dispute)
@router.delete("/{dispute_id}", status_code=204)
async def delete_dispute(
dispute_id: UUID,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Dispute).where(Dispute.id == dispute_id, Dispute.tenant_id == current_user.tenant_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise HTTPException(status_code=404, detail="Dispute not found")
if dispute.status != DisputeStatus.OPEN:
raise HTTPException(status_code=400, detail="Can only delete open disputes")
await db.delete(dispute)
await db.flush()