grantforge-api / backend /endpoints /post_award.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
4.22 kB
"""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}