Phase 3: Dow seed + rejection-routing automation
prisma/seed-dow.ts — idempotent seed for the Dow Jones tenant: - Organization "Dow Jones" (dowjones.com) - 6 ClientTeams: Brand, Events, B2B, Content, Briefing Team, Performance - 3 placeholder Pods (Sergio, Deborah, Shared) — replace with real roster when it's available - Dow Pipeline Template "Dow Jones Standard" with 11 stages: Pipeline → New → Copywriter → Client Review (Copy) → In Progress Creative → Internal Review → Client Feedback → Final Approval → Completed (+ On Hold, Canceled as terminal parking) - Stage dependencies wired (optional stages bypass cleanly so In Progress Creative reaches from New when Copywriter is skipped) - Automation rule "Client Feedback → reopen In Progress Creative": trigger on stage.status_changed where stageSlug=client-feedback and newStatus=CHANGES_REQUESTED. Actions: reopen sibling stage + increment revisionRound, send notification to assignee+producer. - Initial admin user (DOW_ADMIN_EMAIL, default admin@dowjones.com) with bcrypt password and mustChangePassword=true. If DOW_ADMIN_PASSWORD env is unset a secure random is generated and logged once for handoff. - RBAC defaults seeded per role including CLIENT_VIEWER. - Legacy global PipelineStageTemplate rows seeded as FK scaffolding. New action type "reopen_sibling_stage" in action-executor.ts: - Given event.payload.deliverableId + params.siblingSlug, finds the sibling stage (matching either stageDefinition.slug or template.slug) and sets it to params.reopenStatus (default IN_PROGRESS). If params.incrementRound=true, bumps the stage's revisionRound counter and clears completedDate. Added to validateActions' allow-list. Wiring: - package.json db:seed → tsx prisma/seed-dow.ts (HP seed kept at db:seed-legacy for reference until deleted) - prisma.config.ts migrations.seed → seed-dow.ts - bcryptjs + @types/bcryptjs added Verified: tsc --noEmit ✓ zero errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d953cee7ad
commit
2c64356ffd
5 changed files with 613 additions and 4 deletions
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -20,7 +20,9 @@
|
|||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.19",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
@ -5200,6 +5202,12 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
|
|
@ -6494,6 +6502,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/better-result": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:push": "prisma db push",
|
||||
"db:seed": "prisma db seed",
|
||||
"db:seed": "tsx prisma/seed-dow.ts",
|
||||
"db:seed-legacy": "tsx prisma/seed.ts",
|
||||
"db:studio": "prisma studio",
|
||||
"db:seed-tracker": "tsx prisma/seed-tracker-data.ts",
|
||||
"db:seed-team": "tsx prisma/seed.ts",
|
||||
"db:backfill-embeddings": "tsx scripts/backfill-embeddings.ts",
|
||||
"db:clean-slate": "tsx scripts/clean-slate.ts"
|
||||
},
|
||||
|
|
@ -32,7 +32,9 @@
|
|||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.19",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default defineConfig({
|
|||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "tsx prisma/seed.ts",
|
||||
seed: "tsx prisma/seed-dow.ts",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
|
|
|
|||
516
prisma/seed-dow.ts
Normal file
516
prisma/seed-dow.ts
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
/**
|
||||
* Dow Jones Studio Tracker — seed script.
|
||||
*
|
||||
* Seeds the canonical Dow Jones organization, its six client teams, a
|
||||
* placeholder set of production pods, the 11-stage Dow pipeline template
|
||||
* (Pipeline → New → Copywriter → Client Review (Copy) → In Progress Creative
|
||||
* → Internal Review → Client Feedback → Final Approval → Completed +
|
||||
* On Hold + Canceled), the Client-Feedback-rejection automation rule, and
|
||||
* an initial admin user with a one-time temp password.
|
||||
*
|
||||
* Idempotent — safe to re-run. Pass DOW_ADMIN_EMAIL / DOW_ADMIN_PASSWORD
|
||||
* env vars to override the default admin. If DOW_ADMIN_PASSWORD is unset a
|
||||
* random one is generated and logged once to stdout for handoff.
|
||||
*/
|
||||
|
||||
import "dotenv/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "../src/generated/prisma/client";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
// ─── Seed data ─────────────────────────────────────────
|
||||
|
||||
const ORG = {
|
||||
id: "dow-jones-org",
|
||||
name: "Dow Jones",
|
||||
domain: "dowjones.com",
|
||||
};
|
||||
|
||||
const CLIENT_TEAMS = [
|
||||
{ slug: "brand", name: "Brand" },
|
||||
{ slug: "events", name: "Events" },
|
||||
{ slug: "b2b", name: "B2B" },
|
||||
{ slug: "content", name: "Content" },
|
||||
{ slug: "briefing-team", name: "Briefing Team" },
|
||||
{ slug: "performance", name: "Performance" },
|
||||
];
|
||||
|
||||
// Placeholder pods — awaiting the real roster from Zia. Safe to replace.
|
||||
const PODS = [
|
||||
{ slug: "sergio-pod", name: "Sergio's Pod" },
|
||||
{ slug: "deborah-pod", name: "Deborah's Pod" },
|
||||
{ slug: "shared-pod", name: "Shared / Floating" },
|
||||
];
|
||||
|
||||
const DOW_PIPELINE_ID = "dow-pipeline-standard";
|
||||
|
||||
const DOW_STAGES: Array<{
|
||||
slug: string;
|
||||
name: string;
|
||||
order: number;
|
||||
isCriticalGate?: boolean;
|
||||
isOptional?: boolean;
|
||||
color: string;
|
||||
description: string;
|
||||
estimatedDays?: number;
|
||||
}> = [
|
||||
{
|
||||
slug: "pipeline",
|
||||
name: "Pipeline",
|
||||
order: 1,
|
||||
color: "#6B7280",
|
||||
description: "Brief received — awaiting acceptance",
|
||||
estimatedDays: 1,
|
||||
},
|
||||
{
|
||||
slug: "new",
|
||||
name: "New",
|
||||
order: 2,
|
||||
color: "#3B82F6",
|
||||
description: "Brief accepted, not yet assigned",
|
||||
estimatedDays: 1,
|
||||
},
|
||||
{
|
||||
slug: "copywriter",
|
||||
name: "Copywriter",
|
||||
order: 3,
|
||||
isOptional: true,
|
||||
color: "#8B5CF6",
|
||||
description: "Optional copywriting phase",
|
||||
estimatedDays: 2,
|
||||
},
|
||||
{
|
||||
slug: "client-review-copy",
|
||||
name: "Client Review (Copy)",
|
||||
order: 4,
|
||||
isOptional: true,
|
||||
color: "#F59E0B",
|
||||
description: "Optional client sign-off on copy before creative kicks off",
|
||||
estimatedDays: 1,
|
||||
},
|
||||
{
|
||||
slug: "in-progress-creative",
|
||||
name: "In Progress Creative",
|
||||
order: 5,
|
||||
isCriticalGate: true,
|
||||
color: "#2563EB",
|
||||
description: "Active creative production",
|
||||
estimatedDays: 3,
|
||||
},
|
||||
{
|
||||
slug: "internal-review",
|
||||
name: "Internal Review",
|
||||
order: 6,
|
||||
color: "#D97706",
|
||||
description: "Studio QA before sending to client",
|
||||
estimatedDays: 1,
|
||||
},
|
||||
{
|
||||
slug: "client-feedback",
|
||||
name: "Client Feedback",
|
||||
order: 7,
|
||||
color: "#F59E0B",
|
||||
description:
|
||||
"Out for client review. If rejected (CHANGES_REQUESTED), routes back to In Progress Creative automatically.",
|
||||
estimatedDays: 2,
|
||||
},
|
||||
{
|
||||
slug: "final-approval",
|
||||
name: "Final Approval",
|
||||
order: 8,
|
||||
isCriticalGate: true,
|
||||
color: "#16A34A",
|
||||
description: "Client approved — prepare for delivery",
|
||||
estimatedDays: 1,
|
||||
},
|
||||
{
|
||||
slug: "completed",
|
||||
name: "Completed",
|
||||
order: 9,
|
||||
color: "#0D9488",
|
||||
description: "Delivered — done",
|
||||
estimatedDays: 0,
|
||||
},
|
||||
{
|
||||
slug: "on-hold",
|
||||
name: "On Hold",
|
||||
order: 10,
|
||||
isOptional: true,
|
||||
color: "#9CA3AF",
|
||||
description: "Waiting on assets / blocked",
|
||||
estimatedDays: 0,
|
||||
},
|
||||
{
|
||||
slug: "canceled",
|
||||
name: "Canceled",
|
||||
order: 11,
|
||||
isOptional: true,
|
||||
color: "#DC2626",
|
||||
description: "Job canceled",
|
||||
estimatedDays: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// [stageSlug, prerequisiteSlug]
|
||||
const DOW_DEPENDENCIES: [string, string][] = [
|
||||
["new", "pipeline"],
|
||||
["copywriter", "new"],
|
||||
["client-review-copy", "copywriter"],
|
||||
// in-progress-creative depends on "new" so it's reachable when
|
||||
// copywriter/client-review-copy are skipped as optional stages.
|
||||
["in-progress-creative", "new"],
|
||||
["internal-review", "in-progress-creative"],
|
||||
["client-feedback", "internal-review"],
|
||||
["final-approval", "client-feedback"],
|
||||
["completed", "final-approval"],
|
||||
// on-hold and canceled are terminal parking states with no dependencies.
|
||||
];
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
console.log("\n╔══════════════════════════════════════════════╗");
|
||||
console.log("║ Dow Jones Studio Tracker — Seed ║");
|
||||
console.log("╚══════════════════════════════════════════════╝\n");
|
||||
|
||||
// Organization
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { id: ORG.id },
|
||||
update: { name: ORG.name, domain: ORG.domain },
|
||||
create: ORG,
|
||||
});
|
||||
console.log(`✓ Organization: ${org.name} (${org.domain})`);
|
||||
|
||||
// Client Teams
|
||||
for (const team of CLIENT_TEAMS) {
|
||||
await prisma.clientTeam.upsert({
|
||||
where: { organizationId_slug: { organizationId: org.id, slug: team.slug } },
|
||||
update: { name: team.name },
|
||||
create: { ...team, organizationId: org.id },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${CLIENT_TEAMS.length} client teams`);
|
||||
|
||||
// Pods (placeholder — roster TBD)
|
||||
for (const pod of PODS) {
|
||||
await prisma.pod.upsert({
|
||||
where: { organizationId_slug: { organizationId: org.id, slug: pod.slug } },
|
||||
update: { name: pod.name },
|
||||
create: { ...pod, organizationId: org.id },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${PODS.length} pods (placeholder — update with real roster)`);
|
||||
|
||||
// Pipeline Template
|
||||
const pipeline = await prisma.pipelineTemplate.upsert({
|
||||
where: { id: DOW_PIPELINE_ID },
|
||||
update: {
|
||||
name: "Dow Jones Standard",
|
||||
description: "Dow Jones 11-stage pipeline (Pipeline → Completed + On Hold/Canceled)",
|
||||
isDefault: true,
|
||||
isArchived: false,
|
||||
},
|
||||
create: {
|
||||
id: DOW_PIPELINE_ID,
|
||||
name: "Dow Jones Standard",
|
||||
description: "Dow Jones 11-stage pipeline (Pipeline → Completed + On Hold/Canceled)",
|
||||
organizationId: org.id,
|
||||
isDefault: true,
|
||||
isArchived: false,
|
||||
},
|
||||
});
|
||||
console.log(`✓ Pipeline template: ${pipeline.name}`);
|
||||
|
||||
// Stage Definitions
|
||||
const stageMap = new Map<string, string>();
|
||||
for (const stage of DOW_STAGES) {
|
||||
const existing = await prisma.pipelineStageDefinition.upsert({
|
||||
where: {
|
||||
pipelineId_slug: { pipelineId: pipeline.id, slug: stage.slug },
|
||||
},
|
||||
update: {
|
||||
name: stage.name,
|
||||
order: stage.order,
|
||||
isCriticalGate: stage.isCriticalGate ?? false,
|
||||
isOptional: stage.isOptional ?? false,
|
||||
description: stage.description,
|
||||
estimatedDays: stage.estimatedDays ?? null,
|
||||
color: stage.color,
|
||||
},
|
||||
create: {
|
||||
pipelineId: pipeline.id,
|
||||
name: stage.name,
|
||||
slug: stage.slug,
|
||||
order: stage.order,
|
||||
isCriticalGate: stage.isCriticalGate ?? false,
|
||||
isOptional: stage.isOptional ?? false,
|
||||
description: stage.description,
|
||||
estimatedDays: stage.estimatedDays ?? null,
|
||||
color: stage.color,
|
||||
},
|
||||
});
|
||||
stageMap.set(stage.slug, existing.id);
|
||||
}
|
||||
console.log(`✓ Seeded ${DOW_STAGES.length} pipeline stages`);
|
||||
|
||||
// Dependencies — clear & recreate so removals take effect on re-run
|
||||
await prisma.pipelineStageDependencyV2.deleteMany({
|
||||
where: { stage: { pipelineId: pipeline.id } },
|
||||
});
|
||||
for (const [stageSlug, prereqSlug] of DOW_DEPENDENCIES) {
|
||||
await prisma.pipelineStageDependencyV2.create({
|
||||
data: {
|
||||
stageId: stageMap.get(stageSlug)!,
|
||||
prerequisiteId: stageMap.get(prereqSlug)!,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${DOW_DEPENDENCIES.length} stage dependencies`);
|
||||
|
||||
// Rejection-routing automation rule
|
||||
// "When client-feedback stage goes to CHANGES_REQUESTED, reopen the
|
||||
// in-progress-creative stage on the same deliverable and bump revisionRound."
|
||||
//
|
||||
// We need an admin user to own the rule. Create one up front (below) and
|
||||
// then bind the rule to that user.
|
||||
|
||||
// Also keep the legacy-global "Brief Intake" stage templates around so
|
||||
// existing code paths that still FK to PipelineStageTemplate don't break.
|
||||
// (Dow doesn't *use* them — they're compat scaffolding until everything
|
||||
// migrates to PipelineStageDefinition.)
|
||||
await ensureGlobalStageTemplates();
|
||||
|
||||
// Initial admin user
|
||||
const adminEmail = (process.env.DOW_ADMIN_EMAIL || "admin@dowjones.com").toLowerCase();
|
||||
const providedPassword = process.env.DOW_ADMIN_PASSWORD;
|
||||
const tempPassword = providedPassword || generateTempPassword();
|
||||
const passwordHash = await bcrypt.hash(tempPassword, 10);
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: adminEmail },
|
||||
update: {
|
||||
name: "Dow Jones Admin",
|
||||
role: "ADMIN",
|
||||
organizationId: org.id,
|
||||
// Only reset password on first seed — if user already has one, keep it.
|
||||
// Indicated by mustChangePassword still being true.
|
||||
},
|
||||
create: {
|
||||
email: adminEmail,
|
||||
name: "Dow Jones Admin",
|
||||
role: "ADMIN",
|
||||
organizationId: org.id,
|
||||
passwordHash,
|
||||
mustChangePassword: true,
|
||||
isExternal: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Always ensure the admin has a password set (covers pre-existing users
|
||||
// imported without one). Only set if passwordHash is null.
|
||||
const adminRow = await prisma.user.findUnique({
|
||||
where: { id: admin.id },
|
||||
select: { passwordHash: true },
|
||||
});
|
||||
if (!adminRow?.passwordHash) {
|
||||
await prisma.user.update({
|
||||
where: { id: admin.id },
|
||||
data: { passwordHash, mustChangePassword: true },
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✓ Admin user: ${admin.email} (id=${admin.id})`);
|
||||
|
||||
// Automation rule: Client Feedback CHANGES_REQUESTED → reopen creative
|
||||
const rejectionRule = await prisma.automationRule.upsert({
|
||||
where: { id: "dow-rule-client-feedback-rejection" },
|
||||
update: {
|
||||
name: "Client Feedback → reopen In Progress Creative",
|
||||
description:
|
||||
"When a deliverable's Client Feedback stage is marked CHANGES_REQUESTED, automatically reopen the In Progress Creative stage on the same deliverable and increment revisionRound.",
|
||||
isEnabled: true,
|
||||
trigger: {
|
||||
event: "stage.status_changed",
|
||||
conditions: [
|
||||
{ field: "stageSlug", operator: "equals", value: "client-feedback" },
|
||||
{ field: "newStatus", operator: "equals", value: "CHANGES_REQUESTED" },
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: "reopen_sibling_stage",
|
||||
params: {
|
||||
siblingSlug: "in-progress-creative",
|
||||
reopenStatus: "IN_PROGRESS",
|
||||
incrementRound: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "send_notification",
|
||||
params: {
|
||||
title: "Client requested changes",
|
||||
message:
|
||||
"Client rejected {deliverableName} on {projectName}. Creative has been reopened — a new round is in progress.",
|
||||
roles: ["ASSIGNEE", "PRODUCER"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
id: "dow-rule-client-feedback-rejection",
|
||||
name: "Client Feedback → reopen In Progress Creative",
|
||||
description:
|
||||
"When a deliverable's Client Feedback stage is marked CHANGES_REQUESTED, automatically reopen the In Progress Creative stage on the same deliverable and increment revisionRound.",
|
||||
organizationId: org.id,
|
||||
createdById: admin.id,
|
||||
isEnabled: true,
|
||||
trigger: {
|
||||
event: "stage.status_changed",
|
||||
conditions: [
|
||||
{ field: "stageSlug", operator: "equals", value: "client-feedback" },
|
||||
{ field: "newStatus", operator: "equals", value: "CHANGES_REQUESTED" },
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: "reopen_sibling_stage",
|
||||
params: {
|
||||
siblingSlug: "in-progress-creative",
|
||||
reopenStatus: "IN_PROGRESS",
|
||||
incrementRound: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "send_notification",
|
||||
params: {
|
||||
title: "Client requested changes",
|
||||
message:
|
||||
"Client rejected {deliverableName} on {projectName}. Creative has been reopened — a new round is in progress.",
|
||||
roles: ["ASSIGNEE", "PRODUCER"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
console.log(`✓ Automation rule: ${rejectionRule.name}`);
|
||||
|
||||
// RBAC defaults for the Dow org
|
||||
await seedRolePermissions(org.id);
|
||||
|
||||
// ── Handoff banner ──────────────────────────────────
|
||||
console.log("\n─────────────────────────────────────────────────");
|
||||
if (!providedPassword) {
|
||||
console.log(` Admin login (first run — save this now!):`);
|
||||
console.log(` Email: ${adminEmail}`);
|
||||
console.log(` Password: ${tempPassword}`);
|
||||
console.log(` (mustChangePassword=true — forced reset on first login)`);
|
||||
} else {
|
||||
console.log(` Admin: ${adminEmail} — password set from DOW_ADMIN_PASSWORD env`);
|
||||
}
|
||||
console.log("─────────────────────────────────────────────────\n");
|
||||
console.log("Seed complete.\n");
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────
|
||||
|
||||
function generateTempPassword(): string {
|
||||
// 16 char alphanumeric — comfortable to copy from the terminal
|
||||
return randomBytes(12).toString("base64url");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the global PipelineStageTemplate rows referenced by the legacy
|
||||
* DeliverableStage.templateId FK exist. Dow's dynamic pipeline uses
|
||||
* stageDefinitionId; templateId is belt-and-braces scaffolding.
|
||||
*
|
||||
* We seed one global row per Dow stage slug so the upsert path in
|
||||
* deliverable-service can still satisfy the FK. This is cosmetic —
|
||||
* the slugs aren't user-facing for Dow.
|
||||
*/
|
||||
async function ensureGlobalStageTemplates() {
|
||||
for (let i = 0; i < DOW_STAGES.length; i++) {
|
||||
const stage = DOW_STAGES[i];
|
||||
await prisma.pipelineStageTemplate.upsert({
|
||||
where: { slug: stage.slug },
|
||||
update: {
|
||||
name: stage.name,
|
||||
order: i + 1,
|
||||
isCriticalGate: stage.isCriticalGate ?? false,
|
||||
isOptional: stage.isOptional ?? false,
|
||||
description: stage.description,
|
||||
estimatedDays: stage.estimatedDays ?? null,
|
||||
},
|
||||
create: {
|
||||
name: stage.name,
|
||||
slug: stage.slug,
|
||||
order: i + 1,
|
||||
isCriticalGate: stage.isCriticalGate ?? false,
|
||||
isOptional: stage.isOptional ?? false,
|
||||
description: stage.description,
|
||||
estimatedDays: stage.estimatedDays ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_PERMISSIONS: Record<string, string[]> = {
|
||||
ADMIN: [
|
||||
"PROJECT_CREATE", "PROJECT_UPDATE", "PROJECT_DELETE", "PROJECT_VIEW",
|
||||
"DELIVERABLE_VIEW", "DELIVERABLE_CREATE", "DELIVERABLE_UPDATE", "DELIVERABLE_DELETE",
|
||||
"STAGE_VIEW", "STAGE_UPDATE", "STAGE_UPDATE_STATUS", "STAGE_ASSIGN", "STAGE_SCHEDULE",
|
||||
"REVISION_CREATE", "REVISION_UPDATE", "REVISION_REVIEW",
|
||||
"COMMENT_CREATE", "COMMENT_DELETE", "COMMENT_DELETE_ANY",
|
||||
"PIPELINE_MANAGE", "USER_MANAGE", "ROLE_MANAGE", "ORG_SETTINGS",
|
||||
"AUTOMATION_MANAGE", "FIELD_CUSTOMIZE",
|
||||
"CLIENT_TEAM_MANAGE", "POD_MANAGE",
|
||||
],
|
||||
PRODUCER: [
|
||||
"PROJECT_CREATE", "PROJECT_UPDATE", "PROJECT_VIEW",
|
||||
"DELIVERABLE_VIEW", "DELIVERABLE_CREATE", "DELIVERABLE_UPDATE", "DELIVERABLE_DELETE",
|
||||
"STAGE_VIEW", "STAGE_UPDATE", "STAGE_UPDATE_STATUS", "STAGE_ASSIGN", "STAGE_SCHEDULE",
|
||||
"REVISION_CREATE", "REVISION_UPDATE", "REVISION_REVIEW",
|
||||
"COMMENT_CREATE", "COMMENT_DELETE", "COMMENT_DELETE_ANY",
|
||||
"AUTOMATION_MANAGE",
|
||||
],
|
||||
ARTIST: [
|
||||
"PROJECT_VIEW",
|
||||
"DELIVERABLE_VIEW",
|
||||
"STAGE_VIEW", "STAGE_UPDATE_STATUS",
|
||||
"REVISION_CREATE", "REVISION_UPDATE",
|
||||
"COMMENT_CREATE",
|
||||
],
|
||||
CLIENT_VIEWER: [
|
||||
"PROJECT_VIEW",
|
||||
"DELIVERABLE_VIEW",
|
||||
"STAGE_VIEW",
|
||||
"COMMENT_CREATE",
|
||||
],
|
||||
};
|
||||
|
||||
async function seedRolePermissions(organizationId: string) {
|
||||
await prisma.orgRolePermission.deleteMany({ where: { organizationId } });
|
||||
|
||||
const rows: Array<{ organizationId: string; role: any; permission: any }> = [];
|
||||
for (const [role, perms] of Object.entries(DEFAULT_PERMISSIONS)) {
|
||||
for (const permission of perms) {
|
||||
rows.push({ organizationId, role, permission });
|
||||
}
|
||||
}
|
||||
await prisma.orgRolePermission.createMany({ data: rows });
|
||||
console.log(`✓ Seeded ${rows.length} role-permission rows`);
|
||||
}
|
||||
|
||||
// ─── Run ───────────────────────────────────────────────
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("\n❌ Seed failed:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
|
@ -16,7 +16,12 @@ import type { AutomationEvent } from "./event-bus";
|
|||
import { createNotifications } from "@/lib/services/notification-service";
|
||||
|
||||
export interface ActionDefinition {
|
||||
type: "update_stage_status" | "send_notification" | "create_assignment" | "send_webhook";
|
||||
type:
|
||||
| "update_stage_status"
|
||||
| "reopen_sibling_stage"
|
||||
| "send_notification"
|
||||
| "create_assignment"
|
||||
| "send_webhook";
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +43,8 @@ async function executeAction(
|
|||
switch (action.type) {
|
||||
case "update_stage_status":
|
||||
return await executeUpdateStageStatus(action, event);
|
||||
case "reopen_sibling_stage":
|
||||
return await executeReopenSiblingStage(action, event);
|
||||
case "send_notification":
|
||||
return await executeSendNotification(action, event);
|
||||
case "create_assignment":
|
||||
|
|
@ -92,6 +99,72 @@ async function executeUpdateStageStatus(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action: Reopen a sibling stage (same deliverable) identified by slug.
|
||||
*
|
||||
* Used for the Dow Client Feedback → In Progress Creative rejection routing:
|
||||
* when `client-feedback` goes to CHANGES_REQUESTED, put `in-progress-creative`
|
||||
* back into IN_PROGRESS and bump the stage's revisionRound counter.
|
||||
*
|
||||
* params:
|
||||
* siblingSlug — slug of the stage to reopen (e.g. "in-progress-creative")
|
||||
* reopenStatus — status to set (default "IN_PROGRESS")
|
||||
* incrementRound — if true, revisionRound++ on the reopened stage
|
||||
*/
|
||||
async function executeReopenSiblingStage(
|
||||
action: ActionDefinition,
|
||||
event: AutomationEvent
|
||||
): Promise<ActionResult> {
|
||||
const deliverableId = event.payload.deliverableId;
|
||||
const siblingSlug: string | undefined = action.params.siblingSlug;
|
||||
const reopenStatus = action.params.reopenStatus || "IN_PROGRESS";
|
||||
const incrementRound: boolean = action.params.incrementRound === true;
|
||||
|
||||
if (!deliverableId || !siblingSlug) {
|
||||
return {
|
||||
actionType: action.type,
|
||||
success: false,
|
||||
detail: "Missing deliverableId (from event) or siblingSlug (from params)",
|
||||
};
|
||||
}
|
||||
|
||||
// Find the sibling stage by slug (match either the dynamic pipeline
|
||||
// definition slug or the legacy global template slug).
|
||||
const sibling = await prisma.deliverableStage.findFirst({
|
||||
where: {
|
||||
deliverableId,
|
||||
OR: [
|
||||
{ stageDefinition: { slug: siblingSlug } },
|
||||
{ template: { slug: siblingSlug } },
|
||||
],
|
||||
},
|
||||
select: { id: true, revisionRound: true, status: true },
|
||||
});
|
||||
|
||||
if (!sibling) {
|
||||
return {
|
||||
actionType: action.type,
|
||||
success: false,
|
||||
detail: `No sibling stage with slug="${siblingSlug}" on deliverable ${deliverableId}`,
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.deliverableStage.update({
|
||||
where: { id: sibling.id },
|
||||
data: {
|
||||
status: reopenStatus,
|
||||
...(incrementRound ? { revisionRound: sibling.revisionRound + 1 } : {}),
|
||||
completedDate: null,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
actionType: action.type,
|
||||
success: true,
|
||||
detail: `Reopened sibling "${siblingSlug}" on deliverable ${deliverableId} (${reopenStatus}${incrementRound ? ", round++" : ""})`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action: Send a notification to specified users or roles.
|
||||
*/
|
||||
|
|
@ -436,6 +509,7 @@ export function validateActions(
|
|||
|
||||
const validTypes = [
|
||||
"update_stage_status",
|
||||
"reopen_sibling_stage",
|
||||
"send_notification",
|
||||
"create_assignment",
|
||||
"send_webhook",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue