"""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 @router.get("/{project_id}/post-award/milestones") 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)} @router.post("/{project_id}/post-award/milestones") 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) @router.patch("/{project_id}/post-award/milestones/{milestone_id}") 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) @router.delete("/{project_id}/post-award/milestones/{milestone_id}") 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}