151 lines
5.2 KiB
Python
151 lines
5.2 KiB
Python
"""Deal API endpoints backed by DealService with inline payload schemas."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel
|
|
|
|
from app.api.deps import get_deal_repository, get_deal_service, get_organization_context
|
|
from app.models.deal import DealCreate, DealRead, DealStage, DealStatus
|
|
from app.repositories.deal_repo import DealAccessError, DealQueryParams, DealRepository
|
|
from app.services.deal_service import (
|
|
DealService,
|
|
DealStageTransitionError,
|
|
DealStatusValidationError,
|
|
DealUpdateData,
|
|
)
|
|
from app.services.organization_service import OrganizationContext
|
|
|
|
|
|
class DealCreatePayload(BaseModel):
|
|
contact_id: int
|
|
title: str
|
|
amount: Decimal | None = None
|
|
currency: str | None = None
|
|
owner_id: int | None = None
|
|
|
|
def to_domain(self, *, organization_id: int, fallback_owner: int) -> DealCreate:
|
|
return DealCreate(
|
|
organization_id=organization_id,
|
|
contact_id=self.contact_id,
|
|
owner_id=self.owner_id or fallback_owner,
|
|
title=self.title,
|
|
amount=self.amount,
|
|
currency=self.currency,
|
|
)
|
|
|
|
|
|
class DealUpdatePayload(BaseModel):
|
|
status: DealStatus | None = None
|
|
stage: DealStage | None = None
|
|
amount: Decimal | None = None
|
|
currency: str | None = None
|
|
|
|
|
|
router = APIRouter(prefix="/deals", tags=["deals"])
|
|
|
|
|
|
@router.get("/", response_model=list[DealRead])
|
|
async def list_deals(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
status_filter: list[str] | None = Query(default=None, alias="status"),
|
|
min_amount: Decimal | None = None,
|
|
max_amount: Decimal | None = None,
|
|
stage: str | None = None,
|
|
owner_id: int | None = None,
|
|
order_by: str | None = None,
|
|
order: str | None = Query(default="desc", pattern="^(asc|desc)$"),
|
|
context: OrganizationContext = Depends(get_organization_context),
|
|
repo: DealRepository = Depends(get_deal_repository),
|
|
) -> list[DealRead]:
|
|
"""List deals for the current organization with optional filters."""
|
|
|
|
try:
|
|
statuses_value = [DealStatus(value) for value in status_filter] if status_filter else None
|
|
stage_value = DealStage(stage) if stage else None
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid deal filter"
|
|
) from exc
|
|
|
|
params = DealQueryParams(
|
|
organization_id=context.organization_id,
|
|
page=page,
|
|
page_size=page_size,
|
|
statuses=statuses_value,
|
|
stage=stage_value,
|
|
owner_id=owner_id,
|
|
min_amount=min_amount,
|
|
max_amount=max_amount,
|
|
order_by=order_by,
|
|
order_desc=(order != "asc"),
|
|
)
|
|
try:
|
|
deals = await repo.list(params=params, role=context.role, user_id=context.user_id)
|
|
except DealAccessError as exc:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
|
|
|
return [DealRead.model_validate(deal) for deal in deals]
|
|
|
|
|
|
@router.post("/", response_model=DealRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_deal(
|
|
payload: DealCreatePayload,
|
|
context: OrganizationContext = Depends(get_organization_context),
|
|
service: DealService = Depends(get_deal_service),
|
|
) -> DealRead:
|
|
"""Create a new deal within the current organization."""
|
|
|
|
data = payload.to_domain(
|
|
organization_id=context.organization_id, fallback_owner=context.user_id
|
|
)
|
|
try:
|
|
deal = await service.create_deal(data, context=context)
|
|
except DealAccessError as exc:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
|
except DealStatusValidationError as exc: # pragma: no cover - creation shouldn't trigger
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
|
|
return DealRead.model_validate(deal)
|
|
|
|
|
|
@router.patch("/{deal_id}", response_model=DealRead)
|
|
async def update_deal(
|
|
deal_id: int,
|
|
payload: DealUpdatePayload,
|
|
context: OrganizationContext = Depends(get_organization_context),
|
|
repo: DealRepository = Depends(get_deal_repository),
|
|
service: DealService = Depends(get_deal_service),
|
|
) -> DealRead:
|
|
"""Update deal status, stage, or financial data."""
|
|
|
|
existing = await repo.get(
|
|
deal_id,
|
|
organization_id=context.organization_id,
|
|
role=context.role,
|
|
user_id=context.user_id,
|
|
)
|
|
if existing is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deal not found")
|
|
|
|
updates = DealUpdateData(
|
|
status=payload.status,
|
|
stage=payload.stage,
|
|
amount=payload.amount,
|
|
currency=payload.currency,
|
|
)
|
|
|
|
try:
|
|
deal = await service.update_deal(existing, updates, context=context)
|
|
except DealAccessError as exc:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
|
except DealStageTransitionError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
except DealStatusValidationError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
|
|
return DealRead.model_validate(deal)
|