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
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
// 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();
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
-- Phase 12: Offer Editor — additive schema for tier designations, public pricing,
|
||||||
|
-- archive flag, category/ticket dimensions, structured transformation promise, and
|
||||||
|
-- tier<->services junction. All statements additive/idempotent. No drops/truncates
|
||||||
|
-- (Data Safety LOCKED).
|
||||||
|
|
||||||
|
-- offer_macros: archive flag + short description + category/ticket dimensions +
|
||||||
|
-- structured transformation promise
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS description text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS category text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS ticket text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS is_archived boolean NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS cliente_ideale text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS risultato text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS tempo text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS pain text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS metodo text;
|
||||||
|
|
||||||
|
-- offer_micros: tier designation (A/B/C) + manual public price
|
||||||
|
ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS tier_letter text;
|
||||||
|
ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS public_price numeric(10, 2);
|
||||||
|
|
||||||
|
-- CHECK constraint for tier_letter, added separately so the ADD COLUMN above stays
|
||||||
|
-- idempotent even if the constraint already exists (guarded via DO block).
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- New junction table: tier (offer_micros) <-> unified services catalog.
|
||||||
|
-- Additive replacement for legacy offer_micro_services (untouched, points to offer_services).
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS offer_tier_services_tier_idx ON offer_tier_services USING btree (tier_id);
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1781442000000,
|
"when": 1781442000000,
|
||||||
"tag": "0006_add_tags_table",
|
"tag": "0006_add_tags_table",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781700000000,
|
||||||
|
"tag": "0008_offer_tier_schema",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user