-- ============================================================================ -- STRIPE DIRECT INTEGRATION MIGRATION -- ============================================================================ -- Date: April 3, 2026 -- Purpose: Eliminate Paymenter, integrate Stripe directly into Trinity Console -- Gemini Reviewed: Yes (v2 with Sovereign one-time payment support) -- -- This migration adds: -- 1. stripe_products - Product catalog with recurring/one-time billing types -- 2. webhook_events_processed - Idempotency tracking for webhooks -- 3. subscriptions updates - Lifetime tier support (Sovereign $499) -- 4. Performance indexes -- ============================================================================ -- ============================================================================ -- 1. PRODUCT CATALOG -- ============================================================================ CREATE TABLE IF NOT EXISTS stripe_products ( tier_level INT PRIMARY KEY, tier_name VARCHAR(50) NOT NULL, fire_or_frost VARCHAR(10) NOT NULL CHECK (fire_or_frost IN ('fire', 'frost', 'both')), price_monthly DECIMAL(10,2) NOT NULL, stripe_product_id VARCHAR(255) UNIQUE NOT NULL, stripe_price_id VARCHAR(255) UNIQUE NOT NULL, benefits JSONB, billing_type VARCHAR(20) DEFAULT 'recurring' CHECK (billing_type IN ('recurring', 'one-time')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE stripe_products IS 'Stripe product catalog - maps tier levels to Stripe Price IDs'; COMMENT ON COLUMN stripe_products.billing_type IS 'recurring for monthly subscriptions (tiers 1-9), one-time for Sovereign (tier 10)'; COMMENT ON COLUMN stripe_products.price_monthly IS 'Monthly price for recurring, total price for one-time'; -- ============================================================================ -- 2. WEBHOOK IDEMPOTENCY TRACKING -- ============================================================================ CREATE TABLE IF NOT EXISTS webhook_events_processed ( stripe_event_id VARCHAR(255) PRIMARY KEY, event_type VARCHAR(100) NOT NULL, processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_webhook_processed_at ON webhook_events_processed(processed_at); COMMENT ON TABLE webhook_events_processed IS 'Tracks processed Stripe webhook events to ensure idempotency'; -- ============================================================================ -- 3. SUBSCRIPTIONS TABLE UPDATES (Lifetime Support) -- ============================================================================ -- Add lifetime flag and payment intent ID for one-time payments ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS is_lifetime BOOLEAN DEFAULT FALSE; ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS stripe_payment_intent_id VARCHAR(255); -- Add unique constraints for both subscription IDs and payment intent IDs -- Note: We use separate constraints because a record will have EITHER sub_id OR payment_intent_id, never both DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'unique_stripe_sub' ) THEN ALTER TABLE subscriptions ADD CONSTRAINT unique_stripe_sub UNIQUE (stripe_subscription_id); END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'unique_stripe_pi' ) THEN ALTER TABLE subscriptions ADD CONSTRAINT unique_stripe_pi UNIQUE (stripe_payment_intent_id); END IF; END $$; COMMENT ON COLUMN subscriptions.is_lifetime IS 'TRUE for Sovereign tier (one-time $499), FALSE for recurring subscriptions'; COMMENT ON COLUMN subscriptions.stripe_payment_intent_id IS 'Payment Intent ID for one-time payments (Sovereign), NULL for recurring'; -- ============================================================================ -- 4. PERFORMANCE INDEXES -- ============================================================================ -- Index for webhook lookups by customer ID CREATE INDEX IF NOT EXISTS idx_stripe_customer_id ON subscriptions(stripe_customer_id); -- Index for grace period queries (excludes lifetime users) CREATE INDEX IF NOT EXISTS idx_grace_period_lookup ON subscriptions(grace_period_ends_at, is_lifetime) WHERE grace_period_ends_at IS NOT NULL; -- ============================================================================ -- 5. VALIDATION & CONSTRAINTS -- ============================================================================ -- Ensure either subscription_id OR payment_intent_id exists, but not both DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'check_stripe_id_exists' ) THEN ALTER TABLE subscriptions ADD CONSTRAINT check_stripe_id_exists CHECK ( (stripe_subscription_id IS NOT NULL AND stripe_payment_intent_id IS NULL) OR (stripe_subscription_id IS NULL AND stripe_payment_intent_id IS NOT NULL) ); END IF; END $$; COMMENT ON CONSTRAINT check_stripe_id_exists ON subscriptions IS 'Ensures exactly one of subscription_id or payment_intent_id exists'; -- ============================================================================ -- 6. MIGRATION VERIFICATION QUERIES -- ============================================================================ -- Run these to verify migration succeeded: -- SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename IN ('stripe_products', 'webhook_events_processed'); -- SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'subscriptions' AND column_name IN ('is_lifetime', 'stripe_payment_intent_id'); -- SELECT indexname FROM pg_indexes WHERE tablename = 'subscriptions' AND indexname IN ('idx_stripe_customer_id', 'idx_grace_period_lookup'); -- ============================================================================ -- END OF MIGRATION -- ============================================================================