"""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)