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

211 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
from app.models.user import User
from app.models.activity import Activity
router = APIRouter()
class MeetingCreate(Schema):
lead_id: Optional[UUID] = None
contact_id: Optional[UUID] = None
title: str
description: Optional[str] = None
scheduled_at: datetime
duration_minutes: int = 30
location: Optional[str] = None
meeting_url: Optional[str] = None
notes: Optional[str] = None
class MeetingUpdate(Schema):
title: Optional[str] = None
description: Optional[str] = None
scheduled_at: Optional[datetime] = None
duration_minutes: Optional[int] = None
location: Optional[str] = None
meeting_url: Optional[str] = None
status: Optional[str] = None
notes: Optional[str] = None
class MeetingResponse(Schema):
id: UUID
tenant_id: UUID
lead_id: Optional[UUID] = None
title: Optional[str] = None
description: Optional[str] = None
type: str
status: Optional[str] = None
notes: Optional[str] = None
metadata: Optional[dict] = None
created_at: datetime
model_config = {"from_attributes": True}
class MeetingListResponse(Schema):
items: list[MeetingResponse]
total: int
page: int
per_page: int
@router.get("", response_model=MeetingListResponse)
async def list_meetings(
lead_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(Activity).where(
Activity.tenant_id == current_user.tenant_id,
Activity.type == "meeting",
)
if lead_id:
query = query.where(Activity.lead_id == lead_id)
if status:
query = query.where(Activity.status == status)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
query = query.order_by(Activity.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
result = await db.execute(query)
items = [MeetingResponse.model_validate(a) for a in result.scalars().all()]
return MeetingListResponse(items=items, total=total, page=page, per_page=per_page)
@router.get("/{meeting_id}", response_model=MeetingResponse)
async def get_meeting(
meeting_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Activity).where(Activity.id == meeting_id, Activity.tenant_id == current_user.tenant_id, Activity.type == "meeting")
)
meeting = result.scalar_one_or_none()
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
return MeetingResponse.model_validate(meeting)
@router.post("", response_model=MeetingResponse, status_code=201)
async def create_meeting(
data: MeetingCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
meeting = Activity(
tenant_id=current_user.tenant_id,
lead_id=data.lead_id,
user_id=current_user.id,
type="meeting",
title=data.title,
description=data.description,
status="scheduled",
notes=data.notes,
metadata={
"scheduled_at": data.scheduled_at.isoformat(),
"duration_minutes": data.duration_minutes,
"location": data.location,
"meeting_url": data.meeting_url,
"contact_id": str(data.contact_id) if data.contact_id else None,
},
)
db.add(meeting)
await db.flush()
await db.refresh(meeting)
return MeetingResponse.model_validate(meeting)
@router.put("/{meeting_id}", response_model=MeetingResponse)
async def update_meeting(
meeting_id: UUID,
data: MeetingUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Activity).where(Activity.id == meeting_id, Activity.tenant_id == current_user.tenant_id, Activity.type == "meeting")
)
meeting = result.scalar_one_or_none()
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
update_fields = data.model_dump(exclude_none=True)
meta_fields = {"scheduled_at", "duration_minutes", "location", "meeting_url"}
current_meta = meeting.metadata or {}
for key in meta_fields:
if key in update_fields:
val = update_fields.pop(key)
current_meta[key] = val.isoformat() if isinstance(val, datetime) else val
if current_meta:
meeting.metadata = current_meta
for field, value in update_fields.items():
setattr(meeting, field, value)
await db.flush()
await db.refresh(meeting)
return MeetingResponse.model_validate(meeting)
@router.post("/{meeting_id}/confirm", response_model=MeetingResponse)
async def confirm_meeting(
meeting_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Activity).where(Activity.id == meeting_id, Activity.tenant_id == current_user.tenant_id, Activity.type == "meeting")
)
meeting = result.scalar_one_or_none()
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
meeting.status = "confirmed"
await db.flush()
await db.refresh(meeting)
return MeetingResponse.model_validate(meeting)
@router.post("/{meeting_id}/no-show", response_model=MeetingResponse)
async def mark_no_show(
meeting_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Activity).where(Activity.id == meeting_id, Activity.tenant_id == current_user.tenant_id, Activity.type == "meeting")
)
meeting = result.scalar_one_or_none()
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
meeting.status = "no_show"
await db.flush()
await db.refresh(meeting)
return MeetingResponse.model_validate(meeting)
@router.delete("/{meeting_id}", status_code=204)
async def delete_meeting(
meeting_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Activity).where(Activity.id == meeting_id, Activity.tenant_id == current_user.tenant_id, Activity.type == "meeting")
)
meeting = result.scalar_one_or_none()
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
await db.delete(meeting)
await db.flush()