Spaces:
Running
Running
| """Post-award milestone API.""" | |
| from __future__ import annotations | |
| import datetime | |
| from typing import Optional | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from pydantic import BaseModel, Field | |
| from sqlalchemy.orm import Session | |
| from core.post_award.service import ( | |
| create_milestone, | |
| delete_milestone, | |
| list_milestones, | |
| milestone_to_dict, | |
| update_milestone, | |
| ) | |
| from core.subscription.middleware import verify_token | |
| from endpoints.projects import get_db | |
| router = APIRouter(prefix="/api/projects", tags=["post-award"]) | |
| class MilestoneCreateBody(BaseModel): | |
| title: str = Field(..., min_length=1, max_length=200) | |
| due_date: str = Field(..., description="ISO datetime") | |
| description: Optional[str] = None | |
| status: str = "pending" | |
| class MilestoneUpdateBody(BaseModel): | |
| title: Optional[str] = Field(None, min_length=1, max_length=200) | |
| due_date: Optional[str] = None | |
| description: Optional[str] = None | |
| status: Optional[str] = None | |
| def _tenant_id(token_data: dict) -> str: | |
| return token_data.get("sub") or token_data.get("user_id") or "anonymous" | |
| def _parse_iso(value: str) -> datetime.datetime: | |
| try: | |
| dt = datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail="Nieprawidłowy format daty (ISO).") from exc | |
| if dt.tzinfo is None: | |
| dt = dt.replace(tzinfo=datetime.timezone.utc) | |
| return dt | |
| async def get_milestones( | |
| project_id: str, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| rows = list_milestones(db, project_id, clerk_id) | |
| if rows == []: | |
| from core.projects.models import Project | |
| exists = ( | |
| db.query(Project) | |
| .filter(Project.id == project_id, Project.clerk_user_id == clerk_id) | |
| .first() | |
| ) | |
| if not exists: | |
| raise HTTPException(status_code=404, detail="Projekt nie znaleziony") | |
| return {"milestones": [milestone_to_dict(r) for r in rows], "count": len(rows)} | |
| async def add_milestone( | |
| project_id: str, | |
| body: MilestoneCreateBody, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| try: | |
| row = create_milestone( | |
| db, | |
| project_id, | |
| clerk_id, | |
| title=body.title, | |
| due_date=_parse_iso(body.due_date), | |
| description=body.description, | |
| status=body.status, | |
| ) | |
| except ValueError: | |
| raise HTTPException(status_code=404, detail="Projekt nie znaleziony") | |
| return milestone_to_dict(row) | |
| async def patch_milestone( | |
| project_id: str, | |
| milestone_id: str, | |
| body: MilestoneUpdateBody, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| fields = body.model_dump(exclude_unset=True) | |
| if "due_date" in fields and fields["due_date"]: | |
| fields["due_date"] = _parse_iso(fields["due_date"]) | |
| row = update_milestone(db, milestone_id, clerk_id, **fields) | |
| if not row or row.project_id != project_id: | |
| raise HTTPException(status_code=404, detail="Kamień milowy nie znaleziony") | |
| return milestone_to_dict(row) | |
| async def remove_milestone( | |
| project_id: str, | |
| milestone_id: str, | |
| token_data: dict = Depends(verify_token), | |
| db: Session = Depends(get_db), | |
| ): | |
| clerk_id = _tenant_id(token_data) | |
| from core.projects.models import ProjectMilestone | |
| row = ( | |
| db.query(ProjectMilestone) | |
| .filter(ProjectMilestone.id == milestone_id, ProjectMilestone.clerk_user_id == clerk_id) | |
| .first() | |
| ) | |
| if not row or row.project_id != project_id: | |
| raise HTTPException(status_code=404, detail="Kamień milowy nie znaleziony") | |
| delete_milestone(db, milestone_id, clerk_id) | |
| return {"status": "removed", "id": milestone_id} | |