diff --git a/app/api/routers/milestones.py b/app/api/routers/milestones.py index a5a5e72..27ee997 100644 --- a/app/api/routers/milestones.py +++ b/app/api/routers/milestones.py @@ -82,7 +82,36 @@ def update_milestone(project_id: int, milestone_id: int, milestone: schemas.Mile if not db_milestone: raise HTTPException(status_code=404, detail="Milestone not found") ensure_can_edit_milestone(db, current_user.id, db_milestone) + + # --- P3.6 Milestone edit restrictions based on status --- + ms_status = db_milestone.status.value if hasattr(db_milestone.status, 'value') else db_milestone.status + + # Terminal states: no edits allowed + if ms_status in ("completed", "closed"): + raise HTTPException( + status_code=400, + detail=f"Cannot edit a milestone that is '{ms_status}'. No modifications are allowed in terminal state." + ) + data = milestone.model_dump(exclude_unset=True) + + # Never allow status changes via PATCH — use action endpoints instead + if "status" in data: + raise HTTPException( + status_code=400, + detail="Milestone status cannot be changed via PATCH. Use the action endpoints (freeze/start/close) instead." + ) + + # Freeze / undergoing: restrict scope-changing fields + SCOPE_FIELDS = {"title", "description", "due_date", "planned_release_date", "depend_on_milestones", "depend_on_tasks"} + if ms_status in ("freeze", "undergoing"): + blocked = SCOPE_FIELDS & set(data.keys()) + if blocked: + raise HTTPException( + status_code=400, + detail=f"Cannot modify scope fields {sorted(blocked)} when milestone is '{ms_status}'. Scope changes are only allowed in 'open' status." + ) + if "depend_on_milestones" in data: data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"]) if data["depend_on_milestones"] else None if "depend_on_tasks" in data: @@ -100,6 +129,9 @@ def delete_milestone(project_id: int, milestone_id: int, db: Session = Depends(g db_milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first() if not db_milestone: raise HTTPException(status_code=404, detail="Milestone not found") + ms_status = db_milestone.status.value if hasattr(db_milestone.status, 'value') else db_milestone.status + if ms_status in ("undergoing", "completed"): + raise HTTPException(status_code=400, detail=f"Cannot delete a milestone that is '{ms_status}'") db.delete(db_milestone) db.commit() return None