feat: enhance deal management with CRUD operations and improve payload handling
Test / test (push) Successful in 12s Details
Test / test (pull_request) Successful in 11s Details

This commit is contained in:
Artem Kashaev 2025-11-27 16:15:47 +05:00
parent a4c3864ef6
commit e6a3a2cc23
3 changed files with 102 additions and 27 deletions

View File

@ -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

View File

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

View File

@ -1,6 +1,4 @@
"""Business logic services."""
from .deal_service import DealService # noqa: F401
from .organization_service import ( # noqa: F401
OrganizationAccessDeniedError,
OrganizationContext,