Files
leocrm/app/api/v1/deals.py
T

176 lines
4.9 KiB
Python
Raw Normal View History

2026-06-03 23:52:10 +00:00
"""Deal API endpoints."""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.deps import get_current_user
from app.models.deal import DealStage
from app.models.user import User
from app.schemas.deal import (
DealCreate,
DealOut,
DealPipelineOut,
DealStageUpdate,
DealUpdate,
)
from app.services.deal_service import (
InvalidAccount,
create_deal as svc_create_deal,
get_deal as svc_get_deal,
get_pipeline,
list_deals as svc_list_deals,
soft_delete_deal as svc_soft_delete_deal,
update_deal as svc_update_deal,
update_stage as svc_update_stage,
)
router = APIRouter(prefix="/deals", tags=["deals"])
@router.post(
"/",
response_model=DealOut,
status_code=status.HTTP_201_CREATED,
summary="Create a new deal",
)
async def create_deal(
payload: DealCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> DealOut:
try:
deal = await svc_create_deal(
db, payload, org_id=current_user.org_id, owner_id=current_user.id
)
except InvalidAccount as e:
raise HTTPException(status_code=404, detail=str(e)) from e
return DealOut.model_validate(deal)
@router.get(
"/",
response_model=list[DealOut],
summary="List deals (paginated, filterable)",
)
async def list_deals(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
stage: Optional[DealStage] = None,
owner_id: Optional[int] = None,
account_id: Optional[int] = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[DealOut]:
deals = await svc_list_deals(
db,
org_id=current_user.org_id,
skip=skip,
limit=limit,
stage=stage.value if stage else None,
owner_id=owner_id,
account_id=account_id,
)
return [DealOut.model_validate(d) for d in deals]
@router.get(
"/pipeline",
response_model=list[DealPipelineOut],
summary="Get pipeline view (deals grouped by stage)",
)
async def get_pipeline_endpoint(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[DealPipelineOut]:
pipeline = await get_pipeline(db, org_id=current_user.org_id)
out: list[DealPipelineOut] = []
for item in pipeline:
out.append(
DealPipelineOut(
stage=DealStage(item["stage"]),
count=int(item["count"]),
total_value=float(item["total_value"]),
)
)
return out
@router.get(
"/{deal_id}",
response_model=DealOut,
summary="Get one deal",
)
async def get_deal(
deal_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> DealOut:
deal = await svc_get_deal(db, deal_id, org_id=current_user.org_id)
if deal is None:
raise HTTPException(status_code=404, detail="Deal not found")
return DealOut.model_validate(deal)
@router.patch(
"/{deal_id}",
response_model=DealOut,
summary="Update a deal (does NOT change stage)",
)
async def update_deal(
deal_id: int,
payload: DealUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> DealOut:
deal = await svc_get_deal(db, deal_id, org_id=current_user.org_id)
if deal is None:
raise HTTPException(status_code=404, detail="Deal not found")
updated = await svc_update_deal(db, deal, payload)
return DealOut.model_validate(updated)
@router.patch(
"/{deal_id}/stage",
response_model=DealOut,
summary="Change a deal's stage (creates DealStageHistory entry)",
)
async def update_deal_stage(
deal_id: int,
payload: DealStageUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> DealOut:
deal = await svc_get_deal(db, deal_id, org_id=current_user.org_id)
if deal is None:
raise HTTPException(status_code=404, detail="Deal not found")
updated = await svc_update_stage(
db, deal, payload.stage, changed_by=current_user.id, reason=payload.reason
)
return DealOut.model_validate(updated)
@router.delete(
"/{deal_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_class=Response,
summary="Soft-delete a deal",
)
async def delete_deal(
deal_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
deal = await svc_get_deal(db, deal_id, org_id=current_user.org_id)
if deal is None:
raise HTTPException(status_code=404, detail="Deal not found")
await svc_soft_delete_deal(db, deal)
return Response(status_code=status.HTTP_204_NO_CONTENT)
__all__ = ["router"]