Root cause of the mismatch: Deliverable.status is a denormalised
column that was only written at create-time (default NOT_STARTED)
and never refreshed when stages moved. The Projects board read it
live and showed "Not Started" while the pipeline ring + dominant-
stage view correctly showed "at Client Feedback (6/11 stages
complete)".
Fix in two parts:
1. New deliverable-status-service with:
- computeDeliverableStatus(stageStatuses[]) — pure function with
the summary rule:
all stages terminal → APPROVED
any IN_REVIEW → IN_REVIEW
any IN_PROGRESS/CHANGES_REQUESTED → IN_PROGRESS
else → NOT_STARTED
ON_HOLD is producer-managed and never overwritten.
- recomputeDeliverableStatus(deliverableId, txClient?) —
executes the rule + writes if different. Accepts an optional
Prisma tx client so callers can run inside their own
transaction.
2. Wired into every stage-write path:
- stage-service.updateStageStatus (single-stage transitions)
- stage-service bulk transaction (bulkUpdateStages) — dedups
touched deliverable IDs so we don't recompute twice.
- stage-transition-service forward + rework (board drag) —
inline inside the same $transaction so the board bucket is
correct on the next refetch.
3. Backfill script scripts/recompute-deliverable-statuses.ts —
one-off sweep to fix existing stale rows:
npx tsx scripts/recompute-deliverable-statuses.ts
Run once after deploy.
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
/**
|
|
* scripts/recompute-deliverable-statuses.ts
|
|
*
|
|
* One-off backfill for Deliverable.status. Sweeps every deliverable,
|
|
* runs the same recompute logic the live write path now uses, and
|
|
* updates any that were stale.
|
|
*
|
|
* Before this commit, Deliverable.status was only written at create
|
|
* time and never refreshed, so live stage activity didn't propagate
|
|
* to the denormalised column. Run this once after deploy to clean
|
|
* up existing rows; from then on the in-app recompute keeps things
|
|
* in sync automatically.
|
|
*
|
|
* npx tsx scripts/recompute-deliverable-statuses.ts
|
|
*/
|
|
|
|
import "dotenv/config";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
import { PrismaClient } from "../src/generated/prisma/client";
|
|
import { computeDeliverableStatus } from "../src/lib/services/deliverable-status-service";
|
|
|
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
async function main() {
|
|
console.log("Sweeping deliverable statuses…");
|
|
|
|
const deliverables = await prisma.deliverable.findMany({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
status: true,
|
|
stages: { select: { status: true } },
|
|
},
|
|
});
|
|
|
|
let scanned = 0;
|
|
let updated = 0;
|
|
let skippedOnHold = 0;
|
|
|
|
for (const d of deliverables) {
|
|
scanned++;
|
|
// Producer-managed park state — never overwrite based on derived
|
|
// values, same as the live recompute helper.
|
|
if (d.status === "ON_HOLD") {
|
|
skippedOnHold++;
|
|
continue;
|
|
}
|
|
const next = computeDeliverableStatus(d.stages.map((s) => s.status));
|
|
if (next !== d.status) {
|
|
await prisma.deliverable.update({
|
|
where: { id: d.id },
|
|
data: { status: next },
|
|
});
|
|
console.log(` ${d.name}: ${d.status} → ${next}`);
|
|
updated++;
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`\nDone. scanned=${scanned} updated=${updated} skipped-on-hold=${skippedOnHold}`
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
})
|
|
.finally(() => prisma.$disconnect());
|