feat(ST-1109.5): auto-mark PBI as DONE when all its stories are DONE on sprint close

Extends completeSprintAction's $transaction with PBI status cascade:
- Pre-transaction: identify PBIs touched by this close (via stories.pbi_id),
  fetch each with all its stories
- Skip PBIs already DONE; skip PBIs with 0 stories
- Mark PBI DONE only when every story (post-decision) is DONE — stories
  outside the sprint are evaluated against their current DB status
- Promote-only: never demotes a PBI that becomes "incomplete" again

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-29 17:10:56 +02:00
parent 878fa161ef
commit a10ccc936e

View file

@ -171,10 +171,28 @@ export async function completeSprintAction(
const stories = await prisma.story.findMany({
where: { id: { in: storyIds }, sprint_id: sprintId, product_id: sprint.product_id },
select: { id: true },
select: { id: true, pbi_id: true },
})
if (stories.length !== storyIds.length) return { error: 'Ongeldige Sprint-afronding' }
const affectedPbiIds = [...new Set(stories.map((s) => s.pbi_id))]
const candidatePbis = await prisma.pbi.findMany({
where: { id: { in: affectedPbiIds }, status: { not: 'DONE' } },
select: { id: true, stories: { select: { id: true, status: true } } },
})
const decisionByStoryId = new Map(entries)
const pbiIdsToMarkDone = candidatePbis
.filter(
(pbi) =>
pbi.stories.length > 0 &&
pbi.stories.every((s) => {
const next = decisionByStoryId.get(s.id) ?? s.status
return next === 'DONE'
})
)
.map((p) => p.id)
await prisma.$transaction([
...entries.map(([storyId, status]) =>
prisma.story.update({
@ -185,6 +203,9 @@ export async function completeSprintAction(
},
})
),
...pbiIdsToMarkDone.map((id) =>
prisma.pbi.update({ where: { id }, data: { status: 'DONE' } })
),
prisma.sprint.update({
where: { id: sprintId },
data: { status: 'COMPLETED', completed_at: new Date() },