template-saas-database-v2/migrations/V20260120_002__migrate_billing_subscriptions.sql
rckrdmrd 27de049441 [TEMPLATE-SAAS-DB] chore: Update audit schema and add migrations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 04:38:47 -06:00

229 lines
7.8 KiB
PL/PgSQL

-- ============================================
-- Migration: V20260120_002
-- Description: Migrate billing.subscriptions structure
-- Changes:
-- - Add stripe_subscription_id, stripe_customer_id columns
-- - Add interval column (ENUM: month, year)
-- - Add trial_start, cancel_at, cancel_reason columns
-- - Add price_amount, currency columns
-- - Rename cancelled_at -> canceled_at
-- - Change billing.subscription_status enum value: 'trial' -> 'trialing'
-- ============================================
-- UP Migration
BEGIN;
-- ============================================
-- 1. Ensure billing_interval ENUM exists
-- ============================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_type
WHERE typname = 'billing_interval'
AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'billing')
) THEN
CREATE TYPE billing.billing_interval AS ENUM ('month', 'year');
RAISE NOTICE 'Created billing.billing_interval enum';
END IF;
END $$;
-- ============================================
-- 2. Fix billing.subscription_status enum (trial -> trialing)
-- ============================================
DO $$
DECLARE
has_trial_value BOOLEAN;
BEGIN
-- Check if 'trial' exists in the enum (needs migration)
SELECT EXISTS (
SELECT 1
FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
JOIN pg_namespace n ON t.typnamespace = n.oid
WHERE n.nspname = 'billing'
AND t.typname = 'subscription_status'
AND e.enumlabel = 'trial'
) INTO has_trial_value;
IF has_trial_value THEN
-- PostgreSQL doesn't allow renaming enum values directly
-- We need to create a new type and migrate
-- Step 1: Create new enum type
CREATE TYPE billing.subscription_status_new AS ENUM ('trialing', 'active', 'past_due', 'cancelled', 'expired');
-- Step 2: Update columns using the old enum
-- First, change column to use text temporarily
ALTER TABLE billing.subscriptions
ALTER COLUMN status TYPE VARCHAR(50) USING status::VARCHAR(50);
-- Step 3: Update 'trial' to 'trialing'
UPDATE billing.subscriptions
SET status = 'trialing'
WHERE status = 'trial';
-- Step 4: Drop old type
DROP TYPE IF EXISTS billing.subscription_status;
-- Step 5: Rename new type
ALTER TYPE billing.subscription_status_new RENAME TO subscription_status;
-- Step 6: Convert column back to enum
ALTER TABLE billing.subscriptions
ALTER COLUMN status TYPE billing.subscription_status
USING status::billing.subscription_status;
-- Step 7: Restore default
ALTER TABLE billing.subscriptions
ALTER COLUMN status SET DEFAULT 'trialing'::billing.subscription_status;
RAISE NOTICE 'Migrated subscription_status enum: trial -> trialing';
END IF;
END $$;
-- ============================================
-- 3. Add Stripe columns if they don't exist
-- ============================================
-- stripe_subscription_id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'stripe_subscription_id'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN stripe_subscription_id VARCHAR(255) UNIQUE;
CREATE INDEX idx_subscriptions_stripe ON billing.subscriptions(stripe_subscription_id);
RAISE NOTICE 'Added stripe_subscription_id column';
END IF;
END $$;
-- stripe_customer_id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'stripe_customer_id'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN stripe_customer_id VARCHAR(255);
RAISE NOTICE 'Added stripe_customer_id column';
END IF;
END $$;
-- ============================================
-- 4. Add interval column
-- ============================================
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'interval'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN "interval" billing.billing_interval DEFAULT 'month';
RAISE NOTICE 'Added interval column';
END IF;
END $$;
-- ============================================
-- 5. Add trial and cancellation columns
-- ============================================
-- trial_start
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'trial_start'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN trial_start TIMESTAMPTZ;
RAISE NOTICE 'Added trial_start column';
END IF;
END $$;
-- cancel_at (scheduled cancellation date)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'cancel_at'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN cancel_at TIMESTAMPTZ;
RAISE NOTICE 'Added cancel_at column';
END IF;
END $$;
-- cancel_reason
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'cancel_reason'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN cancel_reason VARCHAR(500);
RAISE NOTICE 'Added cancel_reason column';
END IF;
END $$;
-- ============================================
-- 6. Add pricing columns
-- ============================================
-- price_amount
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'price_amount'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN price_amount DECIMAL(10, 2);
RAISE NOTICE 'Added price_amount column';
END IF;
END $$;
-- currency
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'currency'
) THEN
ALTER TABLE billing.subscriptions ADD COLUMN currency VARCHAR(3) DEFAULT 'USD';
RAISE NOTICE 'Added currency column';
END IF;
END $$;
-- ============================================
-- 7. Rename cancelled_at -> canceled_at
-- ============================================
DO $$
BEGIN
-- Check if cancelled_at exists (British spelling)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'billing' AND table_name = 'subscriptions' AND column_name = 'cancelled_at'
) THEN
ALTER TABLE billing.subscriptions RENAME COLUMN cancelled_at TO canceled_at;
RAISE NOTICE 'Renamed cancelled_at to canceled_at';
END IF;
END $$;
-- ============================================
-- 8. Update comments
-- ============================================
COMMENT ON COLUMN billing.subscriptions.stripe_subscription_id IS 'Stripe subscription ID for payment tracking';
COMMENT ON COLUMN billing.subscriptions.stripe_customer_id IS 'Stripe customer ID';
COMMENT ON COLUMN billing.subscriptions."interval" IS 'Billing interval: month or year';
COMMENT ON COLUMN billing.subscriptions.trial_start IS 'When the trial period started';
COMMENT ON COLUMN billing.subscriptions.cancel_at IS 'Scheduled cancellation date (end of period)';
COMMENT ON COLUMN billing.subscriptions.cancel_reason IS 'Reason for cancellation';
COMMENT ON COLUMN billing.subscriptions.price_amount IS 'Price amount at subscription time';
COMMENT ON COLUMN billing.subscriptions.currency IS 'Currency code (ISO 4217)';
COMMIT;