feat(07-unified-service-catalog): add migration and validation scripts
Task 2: Idempotent backfill script (migrate-services.ts) - Migrates service_catalog (21 rows) → services with migrated_from='service_catalog' - Migrates offer_services (35 rows) → services with migrated_from='offer_services' - Name collision detection: appends ' (Offer)' suffix to prevent overwrites - Idempotent via (migrated_from, migrated_id) lookups — re-runs are safe no-ops - Categorizes services: 'catalog' for operational pricing, 'offer' for marketing pricing - Maps service_catalog.description → services.description - Maps offer_services.transformation_description → services.description - Maps service_catalog.unit_price → services.unit_price - Maps offer_services.price → services.unit_price Task 3: Validation script (validate-services-migration.ts) - Check 1: Row counts match (service_catalog/offer_services rows == migrated rows) - Check 2: No orphaned migrated_id references (all point to existing source rows) - Check 3: Existing quote_items.service_id FKs remain valid (untouched) - Check 4: Existing offer_micro_services.service_id FKs remain valid (untouched) - Check 5: Report any unresolved name collisions (post-migration integrity) - Exits 0 only if all checks PASS Plus: push-services-migration.ts helper for direct SQL execution when drizzle-kit unavailable. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { db } from "@/db";
|
||||
import { service_catalog, offer_services, services } from "@/db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
async function migrate() {
|
||||
console.log("Starting services unification migration...\n");
|
||||
|
||||
// 1. Backfill from service_catalog (operational pricing, used by quote_items)
|
||||
const catalogRows = await db.select().from(service_catalog);
|
||||
let catalogInserted = 0;
|
||||
let catalogSkipped = 0;
|
||||
|
||||
for (const row of catalogRows) {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(services)
|
||||
.where(and(eq(services.migrated_from, "service_catalog"), eq(services.migrated_id, row.id)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
catalogSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.insert(services).values({
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
unit_price: row.unit_price,
|
||||
category: "catalog",
|
||||
active: row.active,
|
||||
migrated_from: "service_catalog",
|
||||
migrated_id: row.id,
|
||||
});
|
||||
catalogInserted++;
|
||||
}
|
||||
console.log(`service_catalog: ${catalogInserted} inserted, ${catalogSkipped} skipped (already migrated)`);
|
||||
|
||||
// 2. Backfill from offer_services (marketing pricing, used by offer_micro_services)
|
||||
const offerRows = await db.select().from(offer_services);
|
||||
let offerInserted = 0;
|
||||
let offerSkipped = 0;
|
||||
let renamed = 0;
|
||||
|
||||
for (const row of offerRows) {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(services)
|
||||
.where(and(eq(services.migrated_from, "offer_services"), eq(services.migrated_id, row.id)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
offerSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Name collision check against ALL services rows inserted so far (both sources)
|
||||
const collision = await db
|
||||
.select()
|
||||
.from(services)
|
||||
.where(eq(services.name, row.name))
|
||||
.limit(1);
|
||||
|
||||
const finalName = collision.length > 0 ? `${row.name} (Offer)` : row.name;
|
||||
if (collision.length > 0) renamed++;
|
||||
|
||||
await db.insert(services).values({
|
||||
name: finalName,
|
||||
description: row.transformation_description,
|
||||
unit_price: row.price,
|
||||
category: "offer",
|
||||
active: row.active,
|
||||
migrated_from: "offer_services",
|
||||
migrated_id: row.id,
|
||||
});
|
||||
offerInserted++;
|
||||
}
|
||||
console.log(`offer_services: ${offerInserted} inserted, ${offerSkipped} skipped (already migrated), ${renamed} renamed for collision`);
|
||||
|
||||
console.log("\nMigration complete. Run scripts/validate-services-migration.ts next.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error("Migration failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user