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
This commit is contained in:
zhi
2026-03-30 10:46:20 +00:00
parent cb0be05246
commit c84884fe64
4 changed files with 58 additions and 1 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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