-- ============================================================ -- Migration: Unified Billing System -- Timestamp: 20260322260000 -- Covers: billing_companies, billing_clients, billing_invoices, -- billing_line_items, billing_payments, billing_sync_log, -- ad_campaigns table, billing settings -- ============================================================ -- ─── 1. billing_companies ──────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, legal_name TEXT NOT NULL, llc_entity TEXT, address TEXT, email TEXT, invoice_prefix TEXT NOT NULL DEFAULT 'INV-', stripe_account_id TEXT, mooninvoice_company_id TEXT, mooninvoice_api_key TEXT, stripe_secret_key TEXT, stripe_publishable_key TEXT, default_payment_terms TEXT NOT NULL DEFAULT 'Net 30', is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_billing_companies_active ON public.billing_companies(is_active); ALTER TABLE public.billing_companies ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_companies" ON public.billing_companies; CREATE POLICY "admin_manage_billing_companies" ON public.billing_companies FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 2. billing_clients ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_clients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES public.billing_companies(id) ON DELETE CASCADE, name TEXT NOT NULL, organization TEXT, email TEXT, phone TEXT, billing_address TEXT, notes TEXT, mooninvoice_contact_id TEXT, stripe_customer_id TEXT, total_billed NUMERIC(12,2) NOT NULL DEFAULT 0, total_paid NUMERIC(12,2) NOT NULL DEFAULT 0, invoice_consolidation BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_billing_clients_company_id ON public.billing_clients(company_id); CREATE INDEX IF NOT EXISTS idx_billing_clients_mooninvoice_id ON public.billing_clients(mooninvoice_contact_id); ALTER TABLE public.billing_clients ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_clients" ON public.billing_clients; CREATE POLICY "admin_manage_billing_clients" ON public.billing_clients FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 3. billing_invoices ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES public.billing_companies(id) ON DELETE CASCADE, client_id UUID REFERENCES public.billing_clients(id) ON DELETE SET NULL, invoice_number TEXT NOT NULL, issue_date DATE, due_date DATE, status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','sent','viewed','paid','overdue','cancelled')), subtotal NUMERIC(12,2) NOT NULL DEFAULT 0, tax NUMERIC(12,2) NOT NULL DEFAULT 0, total NUMERIC(12,2) NOT NULL DEFAULT 0, amount_paid NUMERIC(12,2) NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', notes TEXT, internal_memo TEXT, stripe_payment_link TEXT, mooninvoice_invoice_id TEXT, pdf_url TEXT, paid_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_billing_invoices_company_id ON public.billing_invoices(company_id); CREATE INDEX IF NOT EXISTS idx_billing_invoices_client_id ON public.billing_invoices(client_id); CREATE INDEX IF NOT EXISTS idx_billing_invoices_status ON public.billing_invoices(status); CREATE INDEX IF NOT EXISTS idx_billing_invoices_mooninvoice_id ON public.billing_invoices(mooninvoice_invoice_id); CREATE INDEX IF NOT EXISTS idx_billing_invoices_due_date ON public.billing_invoices(due_date); ALTER TABLE public.billing_invoices ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_invoices" ON public.billing_invoices; CREATE POLICY "admin_manage_billing_invoices" ON public.billing_invoices FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 4. billing_line_items ─────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_line_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID REFERENCES public.billing_invoices(id) ON DELETE CASCADE, description TEXT NOT NULL, quantity NUMERIC(10,2) NOT NULL DEFAULT 1, rate NUMERIC(12,2) NOT NULL DEFAULT 0, amount NUMERIC(12,2) NOT NULL DEFAULT 0, campaign_id UUID, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_billing_line_items_invoice_id ON public.billing_line_items(invoice_id); CREATE INDEX IF NOT EXISTS idx_billing_line_items_campaign_id ON public.billing_line_items(campaign_id); ALTER TABLE public.billing_line_items ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_line_items" ON public.billing_line_items; CREATE POLICY "admin_manage_billing_line_items" ON public.billing_line_items FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 5. billing_payments ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID REFERENCES public.billing_invoices(id) ON DELETE SET NULL, client_id UUID REFERENCES public.billing_clients(id) ON DELETE SET NULL, amount NUMERIC(12,2) NOT NULL DEFAULT 0, payment_date DATE, payment_method TEXT, stripe_charge_id TEXT, stripe_payment_intent_id TEXT, notes TEXT, status TEXT NOT NULL DEFAULT 'succeeded' CHECK (status IN ('succeeded','failed','pending','refunded','disputed')), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_billing_payments_invoice_id ON public.billing_payments(invoice_id); CREATE INDEX IF NOT EXISTS idx_billing_payments_client_id ON public.billing_payments(client_id); CREATE INDEX IF NOT EXISTS idx_billing_payments_stripe_charge ON public.billing_payments(stripe_charge_id); ALTER TABLE public.billing_payments ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_payments" ON public.billing_payments; CREATE POLICY "admin_manage_billing_payments" ON public.billing_payments FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 6. billing_sync_log ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_sync_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), sync_type TEXT NOT NULL, company_id UUID REFERENCES public.billing_companies(id) ON DELETE SET NULL, records_synced INTEGER NOT NULL DEFAULT 0, errors JSONB, started_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running','completed','failed')) ); CREATE INDEX IF NOT EXISTS idx_billing_sync_log_company_id ON public.billing_sync_log(company_id); CREATE INDEX IF NOT EXISTS idx_billing_sync_log_started_at ON public.billing_sync_log(started_at); ALTER TABLE public.billing_sync_log ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_sync_log" ON public.billing_sync_log; CREATE POLICY "admin_manage_billing_sync_log" ON public.billing_sync_log FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 7. billing_settings ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.billing_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), billing_mode TEXT NOT NULL DEFAULT 'mooninvoice' CHECK (billing_mode IN ('mooninvoice','native')), sync_frequency_minutes INTEGER NOT NULL DEFAULT 15, last_sync_rmp TIMESTAMPTZ, last_sync_jtr TIMESTAMPTZ, default_payment_terms TEXT NOT NULL DEFAULT 'Net 30', default_currency TEXT NOT NULL DEFAULT 'USD', late_payment_reminder_enabled BOOLEAN NOT NULL DEFAULT false, late_payment_reminder_days INTEGER NOT NULL DEFAULT 7, auto_send_reminders BOOLEAN NOT NULL DEFAULT false, stripe_test_mode BOOLEAN NOT NULL DEFAULT true, migration_import_status JSONB, migration_last_run TIMESTAMPTZ, migration_verified BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); ALTER TABLE public.billing_settings ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_billing_settings" ON public.billing_settings; CREATE POLICY "admin_manage_billing_settings" ON public.billing_settings FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 8. ad_campaigns table ─────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS public.ad_campaigns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, client_name TEXT, campaign_type TEXT NOT NULL DEFAULT 'banner' CHECK (campaign_type IN ('banner','email','sponsored','newsletter')), site_id INTEGER REFERENCES public.sites(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','pending_approval','approved','active','paused','completed','cancelled')), start_date DATE, end_date DATE, budget NUMERIC(12,2), rate NUMERIC(12,2), notes TEXT, billing_invoice_id UUID REFERENCES public.billing_invoices(id) ON DELETE SET NULL, billing_company_id UUID REFERENCES public.billing_companies(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_ad_campaigns_status ON public.ad_campaigns(status); CREATE INDEX IF NOT EXISTS idx_ad_campaigns_billing_invoice ON public.ad_campaigns(billing_invoice_id); CREATE INDEX IF NOT EXISTS idx_ad_campaigns_billing_company ON public.ad_campaigns(billing_company_id); CREATE INDEX IF NOT EXISTS idx_ad_campaigns_site_id ON public.ad_campaigns(site_id); ALTER TABLE public.ad_campaigns ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "admin_manage_ad_campaigns" ON public.ad_campaigns; CREATE POLICY "admin_manage_ad_campaigns" ON public.ad_campaigns FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('administrator', 'admin') ) ); -- ─── 9. Seed default companies and settings ────────────────────────────────── DO $$ DECLARE rmp_id UUID := gen_random_uuid(); jtr_id UUID := gen_random_uuid(); BEGIN INSERT INTO public.billing_companies (id, name, legal_name, llc_entity, address, email, invoice_prefix, default_payment_terms, is_active) VALUES (rmp_id, 'Relevant Media Properties', 'Relevant Media Properties, LLC', 'RMP', '1309 Coffeen Ave, Suite 1200, Sheridan, WY 82801', 'ryan.salazar@relevantmediaproperties.com', 'RMP-', 'Net 30', true), (jtr_id, 'Just Two Roommates', 'Just Two Roommates, LLC', 'JTR', '1309 Coffeen Ave, Suite 1200, Sheridan, WY 82801', 'ryan.salazar@relevantmediaproperties.com', 'JTR-', 'Net 30', true) ON CONFLICT (id) DO NOTHING; INSERT INTO public.billing_settings (billing_mode, sync_frequency_minutes, default_payment_terms, default_currency) VALUES ('mooninvoice', 15, 'Net 30', 'USD') ON CONFLICT (id) DO NOTHING; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'Seed data insertion failed: %', SQLERRM; END $$;