system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/deals.py
Claude f1852c1121
Add SalesMatic AI Sales SaaS Platform - Complete Foundation
Full-stack AI-powered sales automation platform for Saudi SMEs:

Backend (FastAPI + PostgreSQL):
- Multi-tenant architecture with row-level isolation
- JWT auth with RBAC (owner/manager/agent/admin)
- Lead, Customer, Deal, Pipeline, Activity, Message, Proposal models
- Dashboard analytics API (overview, pipeline, revenue)
- WhatsApp Business API, Email (SMTP/SendGrid), SMS (Unifonic) integrations
- Celery + Redis workers for automated follow-ups and scheduled messages
- Property model for Real Estate module (Riyadh districts)
- Hijri date utilities, Arabic/English localization

Frontend (Next.js + Tailwind):
- Professional Arabic RTL landing page with 10 sections
- Brand identity: SalesMatic (سيلزماتك) with custom SVG logo
- Color system: Trust Blue #0F4C81, Growth Teal #00BFA6, CTA Orange #FF6B35
- IBM Plex Sans Arabic + Inter typography
- Responsive design, dark hero section, pricing table, FAQ

Industry Templates:
- Healthcare/Clinics: pipeline stages, WhatsApp message templates, auto-workflows
- Real Estate Riyadh: 20 districts, property tours, payment plans, matching

Infrastructure:
- Docker Compose (PostgreSQL, Redis, Backend, Celery, Frontend, Nginx)
- Nginx reverse proxy config
- Makefile for common operations

https://claude.ai/code/session_01LLR7jzpyNRwDA9kojtT3CW
2026-03-28 03:06:53 +00:00

111 lines
3.8 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from uuid import UUID
from decimal import Decimal
from app.database import get_db
from app.api.deps import get_current_user
from app.models.user import User
from app.models.deal import Deal
from app.schemas.deal import DealCreate, DealUpdate, DealResponse, StageUpdate, PipelineResponse
router = APIRouter()
PIPELINE_STAGES = ["new", "negotiation", "proposal", "closed_won", "closed_lost"]
@router.get("", response_model=list[DealResponse])
async def list_deals(
stage: str = Query(None),
assigned_to: UUID = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
query = select(Deal).where(Deal.tenant_id == current_user.tenant_id)
if stage:
query = query.where(Deal.stage == stage)
if assigned_to:
query = query.where(Deal.assigned_to == assigned_to)
query = query.order_by(Deal.created_at.desc())
result = await db.execute(query)
return [DealResponse.model_validate(d) for d in result.scalars().all()]
@router.get("/pipeline", response_model=PipelineResponse)
async def get_pipeline(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Deal).where(Deal.tenant_id == current_user.tenant_id))
deals = result.scalars().all()
stages = {s: [] for s in PIPELINE_STAGES}
total_value = Decimal("0")
for deal in deals:
stage_key = deal.stage if deal.stage in stages else "new"
stages[stage_key].append(DealResponse.model_validate(deal))
if deal.value:
total_value += deal.value
return PipelineResponse(stages=stages, total_value=total_value, total_deals=len(deals))
@router.post("", response_model=DealResponse, status_code=201)
async def create_deal(
data: DealCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
deal = Deal(tenant_id=current_user.tenant_id, **data.model_dump(exclude_none=True))
db.add(deal)
await db.flush()
await db.refresh(deal)
return DealResponse.model_validate(deal)
@router.put("/{deal_id}", response_model=DealResponse)
async def update_deal(
deal_id: UUID,
data: DealUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Deal).where(Deal.id == deal_id, Deal.tenant_id == current_user.tenant_id))
deal = result.scalar_one_or_none()
if not deal:
raise HTTPException(status_code=404, detail="Deal not found")
for field, value in data.model_dump(exclude_none=True).items():
setattr(deal, field, value)
await db.flush()
await db.refresh(deal)
return DealResponse.model_validate(deal)
@router.put("/{deal_id}/stage", response_model=DealResponse)
async def update_deal_stage(
deal_id: UUID,
data: StageUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if data.stage not in PIPELINE_STAGES:
raise HTTPException(status_code=400, detail=f"Invalid stage. Must be one of: {PIPELINE_STAGES}")
result = await db.execute(select(Deal).where(Deal.id == deal_id, Deal.tenant_id == current_user.tenant_id))
deal = result.scalar_one_or_none()
if not deal:
raise HTTPException(status_code=404, detail="Deal not found")
deal.stage = data.stage
if data.stage in ("closed_won", "closed_lost"):
deal.closed_at = datetime.now(timezone.utc)
deal.probability = 100 if data.stage == "closed_won" else 0
await db.flush()
await db.refresh(deal)
return DealResponse.model_validate(deal)