createDeliverable: auto-mirror dynamic stages into legacy seed table

DeliverableStage.templateId is a NOT-NULL FK to pipeline_stage_templates
(HP-era seed). On a fresh DB with no seed run, that table is empty —
so even with a dynamic pipeline attached, the FK has nothing to point
at and stage creation silently fails (deliverable lands with 0 stages).

When createDeliverable detects this state, mirror each dynamic stage
definition into pipeline_stage_templates (idempotent upsert on slug)
so the FK is satisfied. No schema change; no operator action required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2026-05-12 20:22:19 -04:00
parent 4aa711c7ae
commit 45dfdcad23

View file

@ -81,10 +81,49 @@ export async function createDeliverable(
const dynamicStages = resolvedTemplate?.stages ?? [];
// Always fetch global templates (needed for templateId FK even with dynamic pipelines)
const globalTemplates = await prisma.pipelineStageTemplate.findMany({
let globalTemplates = await prisma.pipelineStageTemplate.findMany({
include: { dependsOn: true },
orderBy: { order: "asc" },
});
// Bootstrap fallback: when the org has no legacy PipelineStageTemplate
// seed rows (fresh DB, no seed run), the FK from DeliverableStage.templateId
// has nothing to point at. Auto-create a 1:1 mirror of the dynamic
// pipeline's stages so the FK is satisfied without forcing the user to
// run a seed. Idempotent on slug + globally unique constraint.
if (useDynamic && globalTemplates.length === 0) {
for (const def of dynamicStages) {
await prisma.pipelineStageTemplate
.upsert({
where: { slug: def.slug },
create: {
name: def.name,
slug: def.slug,
order: def.order,
isCriticalGate: def.isCriticalGate,
isOptional: def.isOptional,
description: def.description,
estimatedDays: def.estimatedDays,
approvalType: def.approvalType,
},
update: {},
})
.catch((err) => {
// Don't block deliverable creation on a seed-mirror miss —
// log and move on; the fallback below uses the first existing
// row as a last-resort templateId.
console.error(
"[deliverable-service] PipelineStageTemplate mirror upsert failed for slug",
def.slug,
err
);
});
}
globalTemplates = await prisma.pipelineStageTemplate.findMany({
include: { dependsOn: true },
orderBy: { order: "asc" },
});
}
const globalBySlug = new Map(globalTemplates.map((t) => [t.slug, t]));
return prisma.$transaction(async (tx) => {