Files
clienthub/scripts/push-12-offer-tier-schema.ts
T
simone 89d15eeb54 feat(12-01): add migration 0008 for offer tier schema + idempotent push script
- Hand-write 0008_offer_tier_schema.sql: additive ALTER TABLE ADD COLUMN IF NOT EXISTS for offer_macros/offer_micros, guarded DO-block CHECK constraint on tier_letter, CREATE TABLE IF NOT EXISTS offer_tier_services + index
- Append journal entry for 0008_offer_tier_schema following the 0006 entry shape
- Add scripts/push-12-offer-tier-schema.ts: idempotent push script with PRODUCTION DEPLOY NOTE — BLOCKING step for Plan 02 (SSH+docker exec) before Wave 3 query layer
2026-06-14 21:19:10 +02:00

143 lines
4.9 KiB
TypeScript

// PRODUCTION DEPLOY NOTE: This migration (0008_offer_tier_schema.sql) is additive-only
// (ADD COLUMN IF NOT EXISTS, a guarded DO-block CHECK constraint, CREATE TABLE IF NOT
// EXISTS, and CREATE INDEX IF NOT EXISTS — no drops/truncates). Per CLAUDE.md Data
// Safety, this script MUST be run against PRODUCTION via SSH+docker exec BEFORE Phase
// 12 Wave 3 code (query layer reading offer_tier_services/tier_letter/public_price/
// category/ticket) is exercised against production data. This is the BLOCKING step
// for Plan 02. Idempotent — safe to re-run.
// Run: npx tsx scripts/push-12-offer-tier-schema.ts (with prod DATABASE_URL)
import postgres from "postgres";
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
console.log("Pushing offer tier schema migration (0008)...");
// offer_macros: archive flag + short description + category/ticket dimensions +
// structured transformation promise
const offerMacrosColumns: Array<[string, string]> = [
["description", "text"],
["category", "text"],
["ticket", "text"],
["is_archived", "boolean NOT NULL DEFAULT false"],
["cliente_ideale", "text"],
["risultato", "text"],
["tempo", "text"],
["pain", "text"],
["metodo", "text"],
];
for (const [column, type] of offerMacrosColumns) {
try {
await client.unsafe(
`ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS ${column} ${type}`
);
console.log(` ✓ offer_macros.${column} ready`);
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(` ✓ offer_macros.${column} already exists (skipped)`);
} else {
throw err;
}
}
}
// offer_micros: tier designation (A/B/C) + manual public price
const offerMicrosColumns: Array<[string, string]> = [
["tier_letter", "text"],
["public_price", "numeric(10, 2)"],
];
for (const [column, type] of offerMicrosColumns) {
try {
await client.unsafe(
`ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS ${column} ${type}`
);
console.log(` ✓ offer_micros.${column} ready`);
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(` ✓ offer_micros.${column} already exists (skipped)`);
} else {
throw err;
}
}
}
// CHECK constraint for tier_letter, guarded so it's safe to re-run.
try {
await client.unsafe(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'offer_micros_tier_letter_check'
) THEN
ALTER TABLE offer_micros
ADD CONSTRAINT offer_micros_tier_letter_check
CHECK (tier_letter IS NULL OR tier_letter IN ('A', 'B', 'C'));
END IF;
END $$;
`);
console.log(" ✓ offer_micros_tier_letter_check constraint ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_micros_tier_letter_check already exists (skipped)");
} else {
throw err;
}
}
// New junction table: tier (offer_micros) <-> unified services catalog.
try {
await client.unsafe(`
CREATE TABLE IF NOT EXISTS offer_tier_services (
tier_id text NOT NULL REFERENCES offer_micros(id) ON DELETE CASCADE,
service_id text NOT NULL REFERENCES services(id) ON DELETE CASCADE,
created_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (tier_id, service_id)
)
`);
console.log(" ✓ offer_tier_services table ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_tier_services table already exists (skipped)");
} else {
throw err;
}
}
try {
await client.unsafe(`
CREATE INDEX IF NOT EXISTS offer_tier_services_tier_idx ON offer_tier_services USING btree (tier_id)
`);
console.log(" ✓ offer_tier_services_tier_idx index ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_tier_services_tier_idx already exists (skipped)");
} else {
throw err;
}
}
console.log("✓ Migration 0008 (offer tier schema) applied successfully");
process.exit(0);
} catch (err: unknown) {
if (err instanceof Error) {
console.error("Error pushing migration:", err.message);
} else {
console.error("Unknown error:", err);
}
process.exit(1);
} finally {
await client.end();
}
}
push();