import { PrismaClient } from '@prisma/client'; import { Decimal } from '@prisma/client/runtime/library'; const prisma = new PrismaClient(); /** * fix-pr-quantities.ts — Restore corrupted PR quantities from specData * * Bug: analyzeImpact() matched ALL PRs by sourceId, so applyQtyChanged * overwrote every PR's quantityPerUnit with the value from one changed line. * * This script reads specData from the active smeta and restores each PR's * quantityPerUnit and quantity by matching on description. * * Usage: npx tsx scripts/fix-pr-quantities.ts [--dry-run] */ interface SpecLine { description: string; unit: string; quantityPerUnit: number; estimatedPrice: number | null; budgetAmount: number | null; materialCategory: string; sourceReference: string; } function normalizeForMatch(s: string): string { return s.trim().toLowerCase().replace(/,/g, '.').replace(/\s+/g, ' '); } function findSpecLine(prDesc: string, specLines: SpecLine[]): SpecLine | null { const normPR = normalizeForMatch(prDesc); // Exact normalized match const exact = specLines.find(sl => normalizeForMatch(sl.description) === normPR); if (exact) return exact; // Inclusion match const incl = specLines.find(sl => { const normSL = normalizeForMatch(sl.description); return normPR.includes(normSL) || normSL.includes(normPR); }); if (incl) return incl; return null; } async function main() { const dryRun = process.argv.includes('--dry-run'); if (dryRun) console.log('=== DRY RUN MODE — no changes will be made ===\n'); // Find all LAUNCHED orders const orders = await prisma.order.findMany({ where: { status: { in: ['LAUNCHED', 'IN_PRODUCTION', 'QUALITY_CONTROL'] } }, select: { id: true, orderCode: true, quantity: true }, }); console.log(`Found ${orders.length} active orders to check.\n`); let totalFixed = 0; for (const order of orders) { // Get active smeta with specData const smeta = await prisma.smeta.findFirst({ where: { orderId: order.id, status: { in: ['APPROVED', 'LOCKED'] } }, orderBy: { version: 'desc' }, select: { id: true, version: true, specData: true }, }); if (!smeta?.specData) { console.log(`[${order.orderCode}] No active smeta with specData — skipping`); continue; } const specLines = smeta.specData as unknown as SpecLine[]; if (!Array.isArray(specLines) || specLines.length === 0) { console.log(`[${order.orderCode}] specData empty — skipping`); continue; } // Get PRs const prs = await prisma.purchaseRequirement.findMany({ where: { orderId: order.id, sourceType: 'SMETA_IMPORT' }, select: { id: true, description: true, quantity: true, quantityPerUnit: true, unit: true }, }); if (prs.length === 0) { console.log(`[${order.orderCode}] No SMETA_IMPORT PRs — skipping`); continue; } // Check if corrupted: all PRs have same quantityPerUnit const distinctQPU = new Set(prs.map(pr => Number(pr.quantityPerUnit))); if (distinctQPU.size > 2) { console.log(`[${order.orderCode}] PRs have ${distinctQPU.size} distinct qPerUnit values — looks OK, skipping`); continue; } console.log(`[${order.orderCode}] ⚠ Only ${distinctQPU.size} distinct qPerUnit value(s) for ${prs.length} PRs — CORRUPTED, fixing...`); let fixed = 0; let notFound = 0; for (const pr of prs) { const specLine = findSpecLine(pr.description, specLines); if (!specLine) { console.log(` ✗ No specLine match for PR "${pr.description}" — skipping`); notFound++; continue; } const correctQPU = specLine.quantityPerUnit; const correctQty = correctQPU * order.quantity; const currentQPU = Number(pr.quantityPerUnit); if (Math.abs(currentQPU - correctQPU) < 0.0001) { continue; // already correct } console.log(` ✓ "${pr.description}": qPerUnit ${currentQPU} → ${correctQPU}, qty ${Number(pr.quantity)} → ${correctQty}`); if (!dryRun) { await prisma.purchaseRequirement.update({ where: { id: pr.id }, data: { quantityPerUnit: new Decimal(correctQPU), quantity: new Decimal(correctQty), revisionAlert: false, revisionNote: null, }, }); } fixed++; } console.log(` → Fixed ${fixed} PRs, ${notFound} unmatched\n`); totalFixed += fixed; } console.log(`\n=== DONE: ${totalFixed} PRs ${dryRun ? 'would be' : ''} fixed ===`); await prisma.$disconnect(); } main().catch((e) => { console.error(e); prisma.$disconnect(); process.exit(1); });