404 lines
16 KiB
SQL
404 lines
16 KiB
SQL
-- ============================================================
|
|
-- Migration: Ad Ops & Email Marketing System
|
|
-- Timestamp: 20260322270000
|
|
-- Extends: ad_campaigns (from 20260322260000)
|
|
-- Adds: ad_placements, documents, email_lists, email_subscribers,
|
|
-- email_campaigns, email_events, email_suppressions,
|
|
-- adops_inbox_log, adops_pending_invoices
|
|
-- ============================================================
|
|
|
|
-- ─── 1. Extend ad_campaigns with ad ops fields ───────────────────────────────
|
|
ALTER TABLE public.ad_campaigns
|
|
ADD COLUMN IF NOT EXISTS campaign_name TEXT,
|
|
ADD COLUMN IF NOT EXISTS publication_id TEXT,
|
|
ADD COLUMN IF NOT EXISTS placement_zone TEXT,
|
|
ADD COLUMN IF NOT EXISTS banner_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS destination_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS agreed_rate NUMERIC(12,2),
|
|
ADD COLUMN IF NOT EXISTS impressions BIGINT NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS clicks BIGINT NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS ctr NUMERIC(6,4) NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS created_by TEXT,
|
|
ADD COLUMN IF NOT EXISTS insertion_order_file TEXT,
|
|
ADD COLUMN IF NOT EXISTS invoice_id TEXT,
|
|
ADD COLUMN IF NOT EXISTS blackmagic_excluded BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS mooninvoice_invoice_id TEXT,
|
|
ADD COLUMN IF NOT EXISTS mooninvoice_contact_id TEXT,
|
|
ADD COLUMN IF NOT EXISTS approval_screenshot_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS live_screenshot_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS approved_by TEXT,
|
|
ADD COLUMN IF NOT EXISTS deployed_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS deactivated_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS io_waived BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS pending_info_fields JSONB;
|
|
|
|
-- Update status check to include ad ops statuses
|
|
ALTER TABLE public.ad_campaigns
|
|
DROP CONSTRAINT IF EXISTS ad_campaigns_status_check;
|
|
|
|
ALTER TABLE public.ad_campaigns
|
|
ADD CONSTRAINT ad_campaigns_status_check
|
|
CHECK (status IN (
|
|
'draft','pending_info','pending_approval','approved',
|
|
'active','paused','ended','cancelled','completed'
|
|
));
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ad_campaigns_publication ON public.ad_campaigns(publication_id);
|
|
CREATE INDEX IF NOT EXISTS idx_ad_campaigns_start_date ON public.ad_campaigns(start_date);
|
|
CREATE INDEX IF NOT EXISTS idx_ad_campaigns_end_date ON public.ad_campaigns(end_date);
|
|
|
|
-- ─── 2. ad_placements ────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.ad_placements (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_id UUID REFERENCES public.ad_campaigns(id) ON DELETE CASCADE,
|
|
publication_id TEXT NOT NULL,
|
|
zone_id TEXT NOT NULL,
|
|
is_active BOOLEAN NOT NULL DEFAULT false,
|
|
deployed_at TIMESTAMPTZ,
|
|
deactivated_at TIMESTAMPTZ,
|
|
screenshot_url TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ad_placements_campaign_id ON public.ad_placements(campaign_id);
|
|
CREATE INDEX IF NOT EXISTS idx_ad_placements_active ON public.ad_placements(is_active);
|
|
CREATE INDEX IF NOT EXISTS idx_ad_placements_publication ON public.ad_placements(publication_id);
|
|
|
|
ALTER TABLE public.ad_placements ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_ad_placements" ON public.ad_placements;
|
|
CREATE POLICY "admin_manage_ad_placements"
|
|
ON public.ad_placements 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. documents ────────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.adops_documents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_id UUID REFERENCES public.ad_campaigns(id) ON DELETE SET NULL,
|
|
email_campaign_id UUID,
|
|
document_type TEXT NOT NULL DEFAULT 'io' CHECK (document_type IN ('io','invoice','contract','other')),
|
|
file_url TEXT NOT NULL,
|
|
file_name TEXT,
|
|
uploaded_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
uploaded_by TEXT,
|
|
notes TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_adops_documents_campaign_id ON public.adops_documents(campaign_id);
|
|
|
|
ALTER TABLE public.adops_documents ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_adops_documents" ON public.adops_documents;
|
|
CREATE POLICY "admin_manage_adops_documents"
|
|
ON public.adops_documents 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. email_lists ──────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.email_lists (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
list_name TEXT NOT NULL,
|
|
publication_id TEXT NOT NULL,
|
|
subscriber_count INTEGER NOT NULL DEFAULT 0,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
last_updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_email_lists_publication ON public.email_lists(publication_id);
|
|
|
|
ALTER TABLE public.email_lists ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_email_lists" ON public.email_lists;
|
|
CREATE POLICY "admin_manage_email_lists"
|
|
ON public.email_lists 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. email_subscribers ────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.email_subscribers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
list_id UUID REFERENCES public.email_lists(id) ON DELETE CASCADE,
|
|
email TEXT NOT NULL,
|
|
first_name TEXT,
|
|
last_name TEXT,
|
|
subscribed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','unsubscribed','bounced','complained')),
|
|
source TEXT,
|
|
last_opened TIMESTAMPTZ,
|
|
last_clicked TIMESTAMPTZ,
|
|
soft_bounce_count INTEGER NOT NULL DEFAULT 0,
|
|
UNIQUE(list_id, email)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_email_subscribers_list_id ON public.email_subscribers(list_id);
|
|
CREATE INDEX IF NOT EXISTS idx_email_subscribers_email ON public.email_subscribers(email);
|
|
CREATE INDEX IF NOT EXISTS idx_email_subscribers_status ON public.email_subscribers(status);
|
|
|
|
ALTER TABLE public.email_subscribers ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_email_subscribers" ON public.email_subscribers;
|
|
CREATE POLICY "admin_manage_email_subscribers"
|
|
ON public.email_subscribers 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. email_campaigns (distributions) ─────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.adops_email_campaigns (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_name TEXT NOT NULL,
|
|
client_name TEXT,
|
|
publication_id TEXT NOT NULL,
|
|
list_id UUID REFERENCES public.email_lists(id) ON DELETE SET NULL,
|
|
subject TEXT NOT NULL,
|
|
from_name TEXT NOT NULL,
|
|
from_email TEXT NOT NULL,
|
|
reply_to TEXT,
|
|
html_content TEXT,
|
|
send_date DATE,
|
|
send_time_et TEXT DEFAULT '11:00',
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN (
|
|
'draft','pending_info','pending_test','test_sent','pending_approval',
|
|
'approved','scheduled','sending','sent','cancelled','held'
|
|
)),
|
|
approved_by TEXT,
|
|
approved_at TIMESTAMPTZ,
|
|
sent_count INTEGER NOT NULL DEFAULT 0,
|
|
test_sent_at TIMESTAMPTZ,
|
|
insertion_order_file TEXT,
|
|
invoice_id TEXT,
|
|
agreed_rate NUMERIC(12,2),
|
|
mooninvoice_invoice_id TEXT,
|
|
is_newsletter BOOLEAN NOT NULL DEFAULT false,
|
|
newsletter_issue_number INTEGER,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_adops_email_campaigns_publication ON public.adops_email_campaigns(publication_id);
|
|
CREATE INDEX IF NOT EXISTS idx_adops_email_campaigns_status ON public.adops_email_campaigns(status);
|
|
CREATE INDEX IF NOT EXISTS idx_adops_email_campaigns_send_date ON public.adops_email_campaigns(send_date);
|
|
|
|
ALTER TABLE public.adops_email_campaigns ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_adops_email_campaigns" ON public.adops_email_campaigns;
|
|
CREATE POLICY "admin_manage_adops_email_campaigns"
|
|
ON public.adops_email_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')
|
|
)
|
|
);
|
|
|
|
-- ─── 7. email_events ─────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.email_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_id UUID REFERENCES public.adops_email_campaigns(id) ON DELETE CASCADE,
|
|
subscriber_id UUID REFERENCES public.email_subscribers(id) ON DELETE SET NULL,
|
|
event_type TEXT NOT NULL CHECK (event_type IN ('delivered','open','click','bounce','unsubscribe','complaint','failed')),
|
|
event_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
link_url TEXT,
|
|
ip_hash TEXT,
|
|
user_agent TEXT,
|
|
bounce_type TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_email_events_campaign_id ON public.email_events(campaign_id);
|
|
CREATE INDEX IF NOT EXISTS idx_email_events_subscriber_id ON public.email_events(subscriber_id);
|
|
CREATE INDEX IF NOT EXISTS idx_email_events_type ON public.email_events(event_type);
|
|
CREATE INDEX IF NOT EXISTS idx_email_events_timestamp ON public.email_events(event_timestamp);
|
|
|
|
ALTER TABLE public.email_events ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_email_events" ON public.email_events;
|
|
CREATE POLICY "admin_manage_email_events"
|
|
ON public.email_events 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. email_suppressions ───────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.email_suppressions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email TEXT NOT NULL,
|
|
reason TEXT NOT NULL CHECK (reason IN ('unsubscribe','hard_bounce','complaint','manual')),
|
|
suppressed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
publication_id TEXT,
|
|
UNIQUE(email, publication_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_email_suppressions_email ON public.email_suppressions(email);
|
|
CREATE INDEX IF NOT EXISTS idx_email_suppressions_publication ON public.email_suppressions(publication_id);
|
|
|
|
ALTER TABLE public.email_suppressions ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_email_suppressions" ON public.email_suppressions;
|
|
CREATE POLICY "admin_manage_email_suppressions"
|
|
ON public.email_suppressions 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. adops_inbox_log ──────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.adops_inbox_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
message_id TEXT UNIQUE,
|
|
from_email TEXT NOT NULL,
|
|
subject TEXT,
|
|
received_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
request_type TEXT CHECK (request_type IN ('banner','distribution','newsletter','inquiry','unknown','ignored')),
|
|
parsed_data JSONB,
|
|
action_taken TEXT,
|
|
campaign_id UUID REFERENCES public.ad_campaigns(id) ON DELETE SET NULL,
|
|
email_campaign_id UUID REFERENCES public.adops_email_campaigns(id) ON DELETE SET NULL,
|
|
thread_id TEXT,
|
|
is_whitelisted BOOLEAN NOT NULL DEFAULT false,
|
|
processing_status TEXT NOT NULL DEFAULT 'received' CHECK (processing_status IN (
|
|
'received','processing','replied','completed','error','ignored'
|
|
)),
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_adops_inbox_log_from_email ON public.adops_inbox_log(from_email);
|
|
CREATE INDEX IF NOT EXISTS idx_adops_inbox_log_received_at ON public.adops_inbox_log(received_at);
|
|
CREATE INDEX IF NOT EXISTS idx_adops_inbox_log_status ON public.adops_inbox_log(processing_status);
|
|
|
|
ALTER TABLE public.adops_inbox_log ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_adops_inbox_log" ON public.adops_inbox_log;
|
|
CREATE POLICY "admin_manage_adops_inbox_log"
|
|
ON public.adops_inbox_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')
|
|
)
|
|
);
|
|
|
|
-- ─── 10. adops_pending_invoices ──────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.adops_pending_invoices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_id UUID REFERENCES public.ad_campaigns(id) ON DELETE CASCADE,
|
|
client_name TEXT NOT NULL,
|
|
client_email TEXT,
|
|
mooninvoice_contact_id TEXT,
|
|
invoice_data JSONB NOT NULL,
|
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
last_retry_at TIMESTAMPTZ,
|
|
next_retry_at TIMESTAMPTZ,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','retrying','created','failed')),
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_adops_pending_invoices_status ON public.adops_pending_invoices(status);
|
|
CREATE INDEX IF NOT EXISTS idx_adops_pending_invoices_next_retry ON public.adops_pending_invoices(next_retry_at);
|
|
|
|
ALTER TABLE public.adops_pending_invoices ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_adops_pending_invoices" ON public.adops_pending_invoices;
|
|
CREATE POLICY "admin_manage_adops_pending_invoices"
|
|
ON public.adops_pending_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')
|
|
)
|
|
);
|
|
|
|
-- ─── 11. Seed default email lists ────────────────────────────────────────────
|
|
DO $$
|
|
BEGIN
|
|
INSERT INTO public.email_lists (list_name, publication_id, is_active)
|
|
VALUES
|
|
('Broadcast Beat Full List', 'broadcast-beat', true),
|
|
('AV Beat Full List', 'av-beat', true),
|
|
('Backlot Beat Full List', 'backlot-beat', true)
|
|
ON CONFLICT DO NOTHING;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RAISE NOTICE 'Email list seed failed: %', SQLERRM;
|
|
END $$;
|