feat: enhance deal management with CRUD operations and improve payload handling
This commit is contained in:
parent
a4c3864ef6
commit
e6a3a2cc23
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
"""Business logic services."""
|
||||
|
||||
from .deal_service import DealService # noqa: F401
|
||||
from .organization_service import ( # noqa: F401
|
||||
OrganizationAccessDeniedError,
|
||||
OrganizationContext,
|
||||
|
|
|
|||
Loading…
Reference in New Issue