feat(08-02): create Phase 8 migration and validation script

- Create SQL migration 0003_offer_phases_quote_templates.sql
  - Creates offers_phases, offer_phase_services, quotes, leads tables
  - Adds new columns to quote_items, projects, phases (additive-only)
  - Includes proper FK constraints, indexes, and CHECK constraints
  - ZERO DATA LOSS: No drops, no truncates — fully reversible

- Create validation script scripts/validate-phase8-migration.ts
  - Checks all 4 new tables exist
  - Verifies 7 new columns added to existing tables
  - Validates FK references intact
  - Exit code 0 on success, 1 on failure

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 07:15:27 +02:00
parent 37fe5eae48
commit f727954dfb
2 changed files with 348 additions and 0 deletions
@@ -0,0 +1,86 @@
-- Phase 8: Offer Phases & Quote Templates (Additive-First Migration)
-- This migration adds hierarchical offer phase support and quote headers.
-- ZERO DATA LOSS: All changes are additive. No drops, no truncates, no deletes.
-- ROLLBACK SAFE: Existing tables untouched. New FK columns nullable.
-- Create leads table (placeholder for Phase 10 CRM)
-- Used as FK for quotes.lead_id
CREATE TABLE IF NOT EXISTS leads (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_leads_email ON leads(email);
-- Create offer_phases table (hierarchical breakdown of offer micros)
-- One offer_micro → many phases (e.g., Discovery, Strategy, Execution)
CREATE TABLE IF NOT EXISTS offer_phases (
id TEXT PRIMARY KEY,
micro_id TEXT NOT NULL REFERENCES offer_micros(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
duration_weeks INTEGER,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(micro_id, title)
);
CREATE INDEX IF NOT EXISTS idx_offer_phases_micro_id ON offer_phases(micro_id);
-- Create offer_phase_services junction (replaces flat offer_micro_services path for new system)
-- Links phases to unified services table (Phase 7)
CREATE TABLE IF NOT EXISTS offer_phase_services (
phase_id TEXT NOT NULL REFERENCES offer_phases(id) ON DELETE CASCADE,
service_id TEXT NOT NULL REFERENCES services(id) ON DELETE CASCADE,
PRIMARY KEY (phase_id, service_id)
);
CREATE INDEX IF NOT EXISTS idx_offer_phase_services_service_id ON offer_phase_services(service_id);
-- Create quotes table (separate quote header from line items)
-- Quote header tracks: who it's for, status, acceptance, total amount
-- Items linked via quote_items.quote_id FK
CREATE TABLE IF NOT EXISTS quotes (
id TEXT PRIMARY KEY,
lead_id TEXT REFERENCES leads(id) ON DELETE CASCADE,
client_id TEXT REFERENCES clients(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
offer_micro_id TEXT REFERENCES offer_micros(id) ON DELETE SET NULL,
total_amount NUMERIC(10, 2) NOT NULL,
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'viewed', 'accepted', 'rejected')),
accepted_at TIMESTAMP WITH TIME ZONE,
accepted_by_email TEXT,
accepted_by_name TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_quotes_token ON quotes(token);
CREATE INDEX IF NOT EXISTS idx_quotes_client_id ON quotes(client_id);
CREATE INDEX IF NOT EXISTS idx_quotes_lead_id ON quotes(lead_id);
CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status);
-- Update quote_items: add quote context FKs (additive-only, no drops)
-- quote_id: link items to quote header
-- offer_micro_id: which tier this item belongs to (audit trail)
-- offer_phase_id: which phase within tier (audit trail)
ALTER TABLE quote_items ADD COLUMN IF NOT EXISTS quote_id TEXT REFERENCES quotes(id) ON DELETE CASCADE;
ALTER TABLE quote_items ADD COLUMN IF NOT EXISTS offer_micro_id TEXT REFERENCES offer_micros(id) ON DELETE SET NULL;
ALTER TABLE quote_items ADD COLUMN IF NOT EXISTS offer_phase_id TEXT REFERENCES offer_phases(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_quote_items_quote_id ON quote_items(quote_id);
CREATE INDEX IF NOT EXISTS idx_quote_items_offer_micro_id ON quote_items(offer_micro_id);
CREATE INDEX IF NOT EXISTS idx_quote_items_offer_phase_id ON quote_items(offer_phase_id);
-- Update projects: add offer template reference + lead origin tracking
-- offer_id: which offer_micro was used as template for this project
-- created_from_lead_id: which lead this project came from (for audit)
ALTER TABLE projects ADD COLUMN IF NOT EXISTS offer_id TEXT REFERENCES offer_micros(id) ON DELETE SET NULL;
ALTER TABLE projects ADD COLUMN IF NOT EXISTS created_from_lead_id TEXT;
CREATE INDEX IF NOT EXISTS idx_projects_offer_id ON projects(offer_id);
CREATE INDEX IF NOT EXISTS idx_projects_created_from_lead_id ON projects(created_from_lead_id);
-- Update phases: add audit trail to original offer_phase template
-- offer_phase_id: which offer_phase this was created from (for auto-provisioning in Phase 11)
ALTER TABLE phases ADD COLUMN IF NOT EXISTS offer_phase_id TEXT REFERENCES offer_phases(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_phases_offer_phase_id ON phases(offer_phase_id);