diff --git a/app/api/v1/deals/models.py b/app/api/v1/deals/models.py index 3ebb08e..620320f 100644 --- a/app/api/v1/deals/models.py +++ b/app/api/v1/deals/models.py @@ -5,16 +5,29 @@ from decimal import Decimal from pydantic import BaseModel +from app.models.deal import DealCreate, DealStage, DealStatus + 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: str | None = None - stage: str | None = None + status: DealStatus | None = None + stage: DealStage | None = None amount: Decimal | None = None currency: str | None = None diff --git a/app/api/v1/deals/views.py b/app/api/v1/deals/views.py index e10b22d..937b97b 100644 --- a/app/api/v1/deals/views.py +++ b/app/api/v1/deals/views.py @@ -1,11 +1,19 @@ -"""Deal API stubs covering list/create/update operations.""" +"""Deal API endpoints backed by DealService.""" from __future__ import annotations from decimal import Decimal -from fastapi import APIRouter, Depends, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, status -from app.api.deps import get_organization_context +from app.api.deps import get_deal_repository, get_deal_service, get_organization_context +from app.models.deal import DealRead, DealStage, DealStatus +from app.repositories.deal_repo import DealRepository, DealAccessError, DealQueryParams +from app.services.deal_service import ( + DealService, + DealStageTransitionError, + DealStatusValidationError, + DealUpdateData, +) from app.services.organization_service import OrganizationContext from .models import DealCreatePayload, DealUpdatePayload @@ -13,11 +21,7 @@ from .models import DealCreatePayload, DealUpdatePayload router = APIRouter(prefix="/deals", tags=["deals"]) -def _stub(endpoint: str) -> dict[str, str]: - return {"detail": f"{endpoint} is not implemented yet"} - - -@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +@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), @@ -27,30 +31,90 @@ async def list_deals( stage: str | None = None, owner_id: int | None = None, order_by: str | None = None, - order: str | None = Query(default=None, pattern="^(asc|desc)$"), + order: str | None = Query(default="desc", pattern="^(asc|desc)$"), context: OrganizationContext = Depends(get_organization_context), -) -> dict[str, str]: - """Placeholder for deal filtering endpoint.""" - _ = (status_filter, context) - return _stub("GET /deals") + 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("/", status_code=status.HTTP_501_NOT_IMPLEMENTED) +@router.post("/", response_model=DealRead, status_code=status.HTTP_201_CREATED) async def create_deal( payload: DealCreatePayload, context: OrganizationContext = Depends(get_organization_context), -) -> dict[str, str]: - """Placeholder for creating a new deal.""" - _ = (payload, context) - return _stub("POST /deals") + 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}", status_code=status.HTTP_501_NOT_IMPLEMENTED) +@router.patch("/{deal_id}", response_model=DealRead) async def update_deal( deal_id: int, payload: DealUpdatePayload, context: OrganizationContext = Depends(get_organization_context), -) -> dict[str, str]: - """Placeholder for modifying deal status or stage.""" - _ = (deal_id, payload, context) - return _stub("PATCH /deals/{deal_id}") + 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) diff --git a/app/services/__init__.py b/app/services/__init__.py index 3e15215..e235f99 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,6 +1,4 @@ """Business logic services.""" - -from .deal_service import DealService # noqa: F401 from .organization_service import ( # noqa: F401 OrganizationAccessDeniedError, OrganizationContext,