feat: enhance activity and task APIs with improved payload handling and response models
This commit is contained in:
parent
0ecf1295d8
commit
b8958dedbd
|
|
@ -1,11 +1,18 @@
|
||||||
"""Pydantic schemas for activity endpoints."""
|
"""Pydantic schemas for activity endpoints."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityCommentBody(BaseModel):
|
||||||
|
text: str = Field(..., min_length=1, max_length=2000)
|
||||||
|
|
||||||
|
|
||||||
class ActivityCommentPayload(BaseModel):
|
class ActivityCommentPayload(BaseModel):
|
||||||
type: Literal["comment"]
|
type: Literal["comment"] = "comment"
|
||||||
payload: dict[str, Any]
|
payload: ActivityCommentBody
|
||||||
|
|
||||||
|
def extract_text(self) -> str:
|
||||||
|
return self.payload.text.strip()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
"""Activity timeline API stubs."""
|
"""Activity timeline endpoints backed by ActivityService."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
from app.api.deps import get_organization_context
|
from app.api.deps import get_activity_service, get_organization_context
|
||||||
|
from app.models.activity import ActivityRead
|
||||||
|
from app.services.activity_service import (
|
||||||
|
ActivityForbiddenError,
|
||||||
|
ActivityListFilters,
|
||||||
|
ActivityService,
|
||||||
|
ActivityValidationError,
|
||||||
|
)
|
||||||
from app.services.organization_service import OrganizationContext
|
from app.services.organization_service import OrganizationContext
|
||||||
|
|
||||||
from .models import ActivityCommentPayload
|
from .models import ActivityCommentPayload
|
||||||
|
|
@ -11,26 +18,44 @@ from .models import ActivityCommentPayload
|
||||||
router = APIRouter(prefix="/deals/{deal_id}/activities", tags=["activities"])
|
router = APIRouter(prefix="/deals/{deal_id}/activities", tags=["activities"])
|
||||||
|
|
||||||
|
|
||||||
def _stub(endpoint: str) -> dict[str, str]:
|
@router.get("/", response_model=list[ActivityRead])
|
||||||
return {"detail": f"{endpoint} is not implemented yet"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
|
||||||
async def list_activities(
|
async def list_activities(
|
||||||
deal_id: int,
|
deal_id: int,
|
||||||
|
limit: int = Query(50, ge=1, le=200),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
context: OrganizationContext = Depends(get_organization_context),
|
context: OrganizationContext = Depends(get_organization_context),
|
||||||
) -> dict[str, str]:
|
service: ActivityService = Depends(get_activity_service),
|
||||||
"""Placeholder for listing deal activities."""
|
) -> list[ActivityRead]:
|
||||||
_ = (deal_id, context)
|
"""Fetch paginated activities for the deal within the current organization."""
|
||||||
return _stub("GET /deals/{deal_id}/activities")
|
|
||||||
|
filters = ActivityListFilters(deal_id=deal_id, limit=limit, offset=offset)
|
||||||
|
try:
|
||||||
|
activities = await service.list_activities(filters=filters, context=context)
|
||||||
|
except ActivityForbiddenError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
return [ActivityRead.model_validate(activity) for activity in activities]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
@router.post("/", response_model=ActivityRead, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_activity_comment(
|
async def create_activity_comment(
|
||||||
deal_id: int,
|
deal_id: int,
|
||||||
payload: ActivityCommentPayload,
|
payload: ActivityCommentPayload,
|
||||||
context: OrganizationContext = Depends(get_organization_context),
|
context: OrganizationContext = Depends(get_organization_context),
|
||||||
) -> dict[str, str]:
|
service: ActivityService = Depends(get_activity_service),
|
||||||
"""Placeholder for adding a comment activity to a deal."""
|
) -> ActivityRead:
|
||||||
_ = (deal_id, payload, context)
|
"""Add a comment to the deal timeline."""
|
||||||
return _stub("POST /deals/{deal_id}/activities")
|
|
||||||
|
try:
|
||||||
|
activity = await service.add_comment(
|
||||||
|
deal_id=deal_id,
|
||||||
|
author_id=context.user_id,
|
||||||
|
text=payload.extract_text(),
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
except ActivityValidationError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
except ActivityForbiddenError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
return ActivityRead.model_validate(activity)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,36 @@
|
||||||
"""Task API schemas."""
|
"""Task API schemas and helpers."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date, datetime, time, timezone
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.models.task import TaskCreate
|
||||||
|
|
||||||
|
|
||||||
class TaskCreatePayload(BaseModel):
|
class TaskCreatePayload(BaseModel):
|
||||||
deal_id: int
|
deal_id: int
|
||||||
title: str
|
title: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
due_date: date
|
due_date: date | None = None
|
||||||
|
|
||||||
|
def to_domain(self) -> TaskCreate:
|
||||||
|
return TaskCreate(
|
||||||
|
deal_id=self.deal_id,
|
||||||
|
title=self.title,
|
||||||
|
description=self.description,
|
||||||
|
due_date=_date_to_datetime(self.due_date) if self.due_date else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_range_boundary(value: date | None, *, end_of_day: bool) -> datetime | None:
|
||||||
|
"""Convert a date query param to an inclusive datetime boundary."""
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
boundary_time = time(23, 59, 59, 999999) if end_of_day else time(0, 0, 0)
|
||||||
|
return datetime.combine(value, boundary_time, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _date_to_datetime(value: date) -> datetime:
|
||||||
|
return datetime.combine(value, time(0, 0, 0), tzinfo=timezone.utc)
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,62 @@
|
||||||
"""Task API stubs supporting list/create operations."""
|
"""Task API endpoints backed by TaskService."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
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_organization_context, get_task_service
|
||||||
|
from app.models.task import TaskRead
|
||||||
from app.services.organization_service import OrganizationContext
|
from app.services.organization_service import OrganizationContext
|
||||||
|
from app.services.task_service import (
|
||||||
|
TaskDueDateError,
|
||||||
|
TaskForbiddenError,
|
||||||
|
TaskListFilters,
|
||||||
|
TaskOrganizationError,
|
||||||
|
TaskService,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import TaskCreatePayload
|
from .models import TaskCreatePayload, to_range_boundary
|
||||||
|
|
||||||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||||
|
|
||||||
|
|
||||||
def _stub(endpoint: str) -> dict[str, str]:
|
@router.get("/", response_model=list[TaskRead])
|
||||||
return {"detail": f"{endpoint} is not implemented yet"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
|
||||||
async def list_tasks(
|
async def list_tasks(
|
||||||
deal_id: int | None = None,
|
deal_id: int | None = None,
|
||||||
only_open: bool = False,
|
only_open: bool = False,
|
||||||
due_before: date | None = Query(default=None),
|
due_before: date | None = Query(default=None),
|
||||||
due_after: date | None = Query(default=None),
|
due_after: date | None = Query(default=None),
|
||||||
context: OrganizationContext = Depends(get_organization_context),
|
context: OrganizationContext = Depends(get_organization_context),
|
||||||
) -> dict[str, str]:
|
service: TaskService = Depends(get_task_service),
|
||||||
"""Placeholder for task filtering endpoint."""
|
) -> list[TaskRead]:
|
||||||
_ = context
|
"""Filter tasks by deal, state, or due date range."""
|
||||||
return _stub("GET /tasks")
|
|
||||||
|
filters = TaskListFilters(
|
||||||
|
deal_id=deal_id,
|
||||||
|
only_open=only_open,
|
||||||
|
due_before=to_range_boundary(due_before, end_of_day=True),
|
||||||
|
due_after=to_range_boundary(due_after, end_of_day=False),
|
||||||
|
)
|
||||||
|
tasks = await service.list_tasks(filters=filters, context=context)
|
||||||
|
return [TaskRead.model_validate(task) for task in tasks]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
@router.post("/", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_task(
|
async def create_task(
|
||||||
payload: TaskCreatePayload,
|
payload: TaskCreatePayload,
|
||||||
context: OrganizationContext = Depends(get_organization_context),
|
context: OrganizationContext = Depends(get_organization_context),
|
||||||
) -> dict[str, str]:
|
service: TaskService = Depends(get_task_service),
|
||||||
"""Placeholder for creating a task linked to a deal."""
|
) -> TaskRead:
|
||||||
_ = (payload, context)
|
"""Create a task ensuring due-date and ownership constraints."""
|
||||||
return _stub("POST /tasks")
|
|
||||||
|
try:
|
||||||
|
task = await service.create_task(payload.to_domain(), context=context)
|
||||||
|
except TaskDueDateError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
except TaskForbiddenError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
||||||
|
except TaskOrganizationError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
return TaskRead.model_validate(task)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue