From c84884fe64c18b7f1eaef075ba458f4be0a9c24b Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 30 Mar 2026 10:46:20 +0000 Subject: [PATCH] BE-PR-008: add Proposal Accept tracking fields (source_proposal_id, source_essential_id) - Add source_proposal_id and source_essential_id FK columns to Task model - Populate tracking fields during Proposal Accept task generation - Add generated_tasks relationship on Proposal model for reverse lookup - Expose source_proposal_id/source_essential_id in TaskResponse schema - Add GeneratedTaskBrief schema and include generated_tasks in ProposalDetailResponse - Proposal detail endpoint now returns generated story tasks with status --- app/api/routers/proposals.py | 23 +++++++++++++++++++++++ app/models/proposal.py | 8 ++++++++ app/models/task.py | 11 +++++++++++ app/schemas/schemas.py | 17 ++++++++++++++++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/app/api/routers/proposals.py b/app/api/routers/proposals.py index 46e1488..4b03742 100644 --- a/app/api/routers/proposals.py +++ b/app/api/routers/proposals.py @@ -63,6 +63,26 @@ def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials: .all() ) result["essentials"] = [_serialize_essential(e) for e in essentials] + + # BE-PR-008: include tasks generated from this Proposal via Accept + gen_tasks = ( + db.query(Task) + .filter(Task.source_proposal_id == proposal.id) + .order_by(Task.id.asc()) + .all() + ) + result["generated_tasks"] = [ + { + "task_id": t.id, + "task_code": t.task_code, + "task_type": t.task_type or "story", + "task_subtype": t.task_subtype, + "title": t.title, + "status": t.status.value if hasattr(t.status, "value") else t.status, + "source_essential_id": t.source_essential_id, + } + for t in gen_tasks + ] return result @@ -327,6 +347,9 @@ def accept_proposal( reporter_id=proposal.created_by_id or current_user.id, created_by_id=proposal.created_by_id or current_user.id, task_code=task_code, + # BE-PR-008: track which Proposal/Essential generated this task + source_proposal_id=proposal.id, + source_essential_id=essential.id, ) db.add(task) db.flush() # materialise task.id diff --git a/app/models/proposal.py b/app/models/proposal.py index 49eb0a4..80e6fbf 100644 --- a/app/models/proposal.py +++ b/app/models/proposal.py @@ -78,6 +78,14 @@ class Proposal(Base): lazy="select", ) + # BE-PR-008: reverse lookup — story tasks generated from this Proposal + generated_tasks = relationship( + "Task", + foreign_keys="Task.source_proposal_id", + lazy="select", + viewonly=True, + ) + # ---- convenience alias ------------------------------------------------ @hybrid_property def proposal_code(self) -> str | None: diff --git a/app/models/task.py b/app/models/task.py index fdb16b7..9d6b91d 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -37,6 +37,17 @@ class Task(Base): assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True) created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True) + # Proposal Accept tracking (BE-PR-008) + # When a task is generated from Proposal Accept, these record the source. + source_proposal_id = Column( + Integer, ForeignKey("proposes.id"), nullable=True, + comment="Proposal that generated this task via accept (NULL if manually created)", + ) + source_essential_id = Column( + Integer, ForeignKey("essentials.id"), nullable=True, + comment="Essential that generated this task via accept (NULL if manually created)", + ) + # Tags (comma-separated) tags = Column(String(500), nullable=True) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 7d2f511..8835322 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -93,6 +93,9 @@ class TaskResponse(TaskBase): resolution_summary: Optional[str] = None positions: Optional[str] = None pending_matters: Optional[str] = None + # BE-PR-008: Proposal Accept tracking + source_proposal_id: Optional[int] = None + source_essential_id: Optional[int] = None created_at: datetime updated_at: Optional[datetime] = None @@ -344,10 +347,22 @@ class EssentialResponse(EssentialBase): from_attributes = True +class GeneratedTaskBrief(BaseModel): + """Brief info about a story task generated from Proposal Accept.""" + task_id: int + task_code: Optional[str] = None + task_type: str + task_subtype: Optional[str] = None + title: str + status: Optional[str] = None + source_essential_id: Optional[int] = None + + class ProposalDetailResponse(ProposalResponse): - """Extended Proposal response that embeds its Essential list.""" + """Extended Proposal response that embeds its Essential list and generated tasks.""" essentials: List[EssentialResponse] = [] + generated_tasks: List[GeneratedTaskBrief] = [] class Config: from_attributes = True