BE-PR-007: refactor Proposal Accept to generate story tasks from all Essentials
- Removed old logic that created a single story/feature task on accept - Accept now iterates all Essentials under the Proposal - Each Essential.type maps to a story/* task (feature/improvement/refactor) - All tasks created in a single transaction - Added ProposalAcceptResponse and GeneratedTaskSummary schemas - Proposal must have at least one Essential to be accepted - No longer writes to deprecated feat_task_id field
This commit is contained in:
@@ -236,7 +236,7 @@ class AcceptRequest(schemas.BaseModel):
|
||||
milestone_id: int
|
||||
|
||||
|
||||
@router.post("/{proposal_id}/accept", response_model=schemas.ProposalResponse)
|
||||
@router.post("/{proposal_id}/accept", response_model=schemas.ProposalAcceptResponse)
|
||||
def accept_proposal(
|
||||
project_id: str,
|
||||
proposal_id: str,
|
||||
@@ -244,7 +244,16 @@ def accept_proposal(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey),
|
||||
):
|
||||
"""Accept a proposal: create a feature story task in the chosen milestone."""
|
||||
"""Accept a proposal: generate story tasks from all Essentials into the chosen milestone.
|
||||
|
||||
Each Essential under the Proposal produces a corresponding ``story/*`` task:
|
||||
- feature → story/feature
|
||||
- improvement → story/improvement
|
||||
- refactor → story/refactor
|
||||
|
||||
All tasks are created in a single transaction. The Proposal must have at
|
||||
least one Essential to be accepted.
|
||||
"""
|
||||
project = _find_project(db, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
@@ -270,43 +279,86 @@ def accept_proposal(
|
||||
if ms_status != "open":
|
||||
raise HTTPException(status_code=400, detail="Target milestone must be in 'open' status")
|
||||
|
||||
# Generate task code
|
||||
milestone_code = milestone.milestone_code or f"m{milestone.id}"
|
||||
max_task = db.query(Task).filter(Task.milestone_id == milestone.id).order_by(Task.id.desc()).first()
|
||||
next_num = (max_task.id + 1) if max_task else 1
|
||||
task_code = f"{milestone_code}:T{next_num:05x}"
|
||||
|
||||
# Create feature story task
|
||||
task = Task(
|
||||
title=proposal.title,
|
||||
description=proposal.description,
|
||||
task_type="story",
|
||||
task_subtype="feature",
|
||||
status=TaskStatus.PENDING,
|
||||
priority=TaskPriority.MEDIUM,
|
||||
project_id=project.id,
|
||||
milestone_id=milestone.id,
|
||||
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,
|
||||
# Fetch all Essentials for this Proposal
|
||||
essentials = (
|
||||
db.query(Essential)
|
||||
.filter(Essential.proposal_id == proposal.id)
|
||||
.order_by(Essential.id.asc())
|
||||
.all()
|
||||
)
|
||||
db.add(task)
|
||||
db.flush() # get task.id
|
||||
if not essentials:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Proposal has no Essentials. Add at least one Essential before accepting.",
|
||||
)
|
||||
|
||||
# Update proposal
|
||||
# Map Essential type → task subtype
|
||||
ESSENTIAL_TYPE_TO_SUBTYPE = {
|
||||
"feature": "feature",
|
||||
"improvement": "improvement",
|
||||
"refactor": "refactor",
|
||||
}
|
||||
|
||||
# Determine next task number in this milestone
|
||||
milestone_code = milestone.milestone_code or f"m{milestone.id}"
|
||||
max_task = (
|
||||
db.query(sa_func.max(Task.id))
|
||||
.filter(Task.milestone_id == milestone.id)
|
||||
.scalar()
|
||||
)
|
||||
next_num = (max_task + 1) if max_task else 1
|
||||
|
||||
# Create one story task per Essential — all within the current transaction
|
||||
generated_tasks = []
|
||||
for essential in essentials:
|
||||
etype = essential.type.value if hasattr(essential.type, "value") else essential.type
|
||||
task_subtype = ESSENTIAL_TYPE_TO_SUBTYPE.get(etype, "feature")
|
||||
task_code = f"{milestone_code}:T{next_num:05x}"
|
||||
|
||||
task = Task(
|
||||
title=essential.title,
|
||||
description=essential.description,
|
||||
task_type="story",
|
||||
task_subtype=task_subtype,
|
||||
status=TaskStatus.PENDING,
|
||||
priority=TaskPriority.MEDIUM,
|
||||
project_id=project.id,
|
||||
milestone_id=milestone.id,
|
||||
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,
|
||||
)
|
||||
db.add(task)
|
||||
db.flush() # materialise task.id
|
||||
|
||||
generated_tasks.append({
|
||||
"task_id": task.id,
|
||||
"task_code": task_code,
|
||||
"task_type": "story",
|
||||
"task_subtype": task_subtype,
|
||||
"title": essential.title,
|
||||
"essential_id": essential.id,
|
||||
"essential_code": essential.essential_code,
|
||||
})
|
||||
next_num = task.id + 1 # use real id for next code to stay consistent
|
||||
|
||||
# Update proposal status (do NOT write feat_task_id — deprecated)
|
||||
proposal.status = ProposalStatus.ACCEPTED
|
||||
proposal.feat_task_id = str(task.id)
|
||||
|
||||
db.commit()
|
||||
db.refresh(proposal)
|
||||
|
||||
log_activity(db, "accept", "proposal", proposal.id, user_id=current_user.id, details={
|
||||
"milestone_id": milestone.id,
|
||||
"generated_task_id": task.id,
|
||||
"task_code": task_code,
|
||||
"generated_tasks": [
|
||||
{"task_id": t["task_id"], "task_code": t["task_code"], "essential_id": t["essential_id"]}
|
||||
for t in generated_tasks
|
||||
],
|
||||
})
|
||||
|
||||
return _serialize_proposal(db, proposal)
|
||||
result = _serialize_proposal(db, proposal, include_essentials=True)
|
||||
result["generated_tasks"] = generated_tasks
|
||||
return result
|
||||
|
||||
|
||||
class RejectRequest(schemas.BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user