/** * One-time backfill script (SCHEMA-03, D-05) — computes and persists * `Tender.fingerprint` for every pre-existing row. * * The TenderSource row backfill itself lives in the migration SQL * (20260723120000_add_tender_source/migration.sql) because it's a pure * column copy. The fingerprint backfill does NOT belong in SQL: it needs * the umlaut/CPV-normalization logic in tender-fingerprint.ts, which is * code, not SQL (13-RESEARCH.md "Backfill-Migration"). * * Run once, manually, after the migration has been applied (compiled first * — this file uses standard extensionless TS imports, so it must run as * built CommonJS, not as a raw .ts file): * pnpm --filter @tessera/api build * DATABASE_URL=... node dist/tenders/backfill-tender-source.js * * Idempotent: recomputes and overwrites fingerprint for every row it reads, * so re-running is safe (same deterministic output for unchanged inputs). */ import { PrismaClient, Prisma } from '@prisma/client'; import { tenderFingerprint } from './tender-fingerprint'; async function main(): Promise { const prisma = new PrismaClient(); try { const tenders = await prisma.tender.findMany({ select: { id: true, title: true, buyerName: true, cpvDivisions: true, deadlineAt: true, estimatedValue: true, }, }); let updated = 0; for (const t of tenders) { const fingerprint = tenderFingerprint({ buyerName: t.buyerName, title: t.title, cpvDivisions: t.cpvDivisions, deadlineAt: t.deadlineAt, // estimatedValue is Decimal? — convert to number|null before bucketing (T-13-01-03). estimatedValue: decimalToNumberOrNull(t.estimatedValue), }); await prisma.tender.update({ where: { id: t.id }, data: { fingerprint }, }); updated += 1; } console.log(`backfill-tender-source: fingerprint set on ${updated}/${tenders.length} tenders`); } finally { await prisma.$disconnect(); } } function decimalToNumberOrNull(v: Prisma.Decimal | null): number | null { return v === null ? null : Number(v); } main().catch((err) => { console.error('backfill-tender-source failed:', err); process.exitCode = 1; });