diff --git a/scripts/push-12-offer-tier-schema.ts b/scripts/push-12-offer-tier-schema.ts new file mode 100644 index 0000000..39cac2d --- /dev/null +++ b/scripts/push-12-offer-tier-schema.ts @@ -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(); diff --git a/src/db/migrations/0008_offer_tier_schema.sql b/src/db/migrations/0008_offer_tier_schema.sql new file mode 100644 index 0000000..51ef36b --- /dev/null +++ b/src/db/migrations/0008_offer_tier_schema.sql @@ -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); diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 9397988..248fe36 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1781442000000, "tag": "0006_add_tags_table", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1781700000000, + "tag": "0008_offer_tier_schema", + "breakpoints": true } ] } \ No newline at end of file