Files
avbeat-com/supabase/migrations/20260322260000_unified_billing_system.sql
2026-05-07 16:39:17 +00:00

344 lines
14 KiB
SQL

-- ============================================================
-- 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 $$;