From 45dfdcad23fc844d3e127ab8514ea29df73d3a83 Mon Sep 17 00:00:00 2001 From: DJP Date: Tue, 12 May 2026 20:22:19 -0400 Subject: [PATCH] createDeliverable: auto-mirror dynamic stages into legacy seed table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/lib/services/deliverable-service.ts | 41 ++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib/services/deliverable-service.ts b/src/lib/services/deliverable-service.ts index dc5eb92..e4e9001 100644 --- a/src/lib/services/deliverable-service.ts +++ b/src/lib/services/deliverable-service.ts @@ -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) => {