2026-06-03 23:52:10 +00:00
|
|
|
"""Deal API endpoints."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
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,
|
2026-06-10 21:24:24 +00:00
|
|
|
get_pipeline,
|
|
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
create_deal as svc_create_deal,
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
get_deal as svc_get_deal,
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
list_deals as svc_list_deals,
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
soft_delete_deal as svc_soft_delete_deal,
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
update_deal as svc_update_deal,
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
|
|
|
|
from app.services.deal_service import (
|
2026-06-03 23:52:10 +00:00
|
|
|
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),
|
2026-06-10 21:24:24 +00:00
|
|
|
stage: DealStage | None = None,
|
|
|
|
|
owner_id: int | None = None,
|
|
|
|
|
account_id: int | None = None,
|
2026-06-03 23:52:10 +00:00
|
|
|
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"]
|