5fcb574aca
Test / test (push) Successful in 15s
- Reformatted function signatures in `organization_service.py` and `task_service.py` for better alignment. - Updated import statements across multiple files for consistency and organization. - Enhanced test files by improving formatting and ensuring consistent use of async session factories. - Added type hints and improved type safety in various service and test files. - Adjusted `pyproject.toml` to include configuration for isort, mypy, and ruff for better code quality checks. - Cleaned up unused imports and organized existing ones in several test files.
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)
|