diff --git a/app/api/v1/deals.py b/app/api/v1/deals.py new file mode 100644 index 0000000..6a2d63a --- /dev/null +++ b/app/api/v1/deals.py @@ -0,0 +1,175 @@ +"""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"]