330 lines
12 KiB
PL/PgSQL
330 lines
12 KiB
PL/PgSQL
-- Migration 086: Enhance email substitution with brand_settings
|
|
-- Updates send_campaign and send_stop_blast to:
|
|
-- 1. Look up brand_settings for legal_business_name (falls back to brands.name)
|
|
-- 2. Append default_email_signature to outbound emails if set
|
|
-- 3. Support {{address}}, {{phone}}, {{email}}, {{website}} variables
|
|
--
|
|
-- Variable substitution priority:
|
|
-- {{company_name}} → legal_business_name (brand_settings) → brands.name → fallback
|
|
-- {{address}} → street_address, city, state postal_code from brand_settings
|
|
-- {{phone}} → phone from brand_settings
|
|
-- {{email}} → email from brand_settings
|
|
-- {{website}} → website_url from brand_settings
|
|
-- {{signature}} → default_email_signature from brand_settings (appended to body)
|
|
|
|
-- ── Enhanced send_campaign ────────────────────────────────────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION public.send_campaign(p_campaign_id UUID, p_brand_id UUID DEFAULT NULL)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_campaign communication_campaigns;
|
|
v_settings communication_settings;
|
|
v_brand_settings RECORD;
|
|
v_brand_name TEXT := 'Route Commerce';
|
|
v_signature TEXT;
|
|
v_address TEXT;
|
|
v_phone TEXT;
|
|
v_email TEXT;
|
|
v_website TEXT;
|
|
v_body_final TEXT;
|
|
v_subj_final TEXT;
|
|
v_audience JSONB;
|
|
v_entries JSONB := '[]'::JSONB;
|
|
v_customer JSONB;
|
|
v_count INTEGER := 0;
|
|
BEGIN
|
|
SELECT * INTO v_campaign FROM communication_campaigns WHERE id = p_campaign_id;
|
|
IF NOT FOUND THEN
|
|
RETURN jsonb_build_object('success', false, 'error', 'Campaign not found');
|
|
END IF;
|
|
|
|
-- Resolve effective brand_id
|
|
IF p_brand_id IS NOT NULL THEN
|
|
-- Use provided brand_id
|
|
ELSIF v_campaign.brand_id IS NOT NULL THEN
|
|
p_brand_id := v_campaign.brand_id;
|
|
END IF;
|
|
|
|
-- Look up brand_settings for extended variables
|
|
SELECT
|
|
bs.legal_business_name,
|
|
bs.phone,
|
|
bs.email,
|
|
bs.website_url,
|
|
bs.default_email_signature,
|
|
bs.street_address,
|
|
bs.city,
|
|
bs.state,
|
|
bs.postal_code,
|
|
b.name AS brand_name
|
|
INTO v_brand_settings
|
|
FROM brand_settings bs
|
|
JOIN brands b ON b.id = bs.brand_id
|
|
WHERE bs.brand_id = p_brand_id;
|
|
|
|
-- Build brand name: legal_business_name > brand_name > fallback
|
|
v_brand_name := COALESCE(
|
|
v_brand_settings.legal_business_name,
|
|
v_brand_settings.brand_name,
|
|
'Route Commerce'
|
|
);
|
|
|
|
-- Build full address string
|
|
IF v_brand_settings.street_address IS NOT NULL THEN
|
|
v_address := v_brand_settings.street_address
|
|
|| ', ' || COALESCE(v_brand_settings.city, '')
|
|
|| ', ' || COALESCE(v_brand_settings.state, '')
|
|
|| ' ' || COALESCE(v_brand_settings.postal_code, '');
|
|
-- Clean up extra commas from empty parts
|
|
v_address := trim(both ',' from v_address);
|
|
ELSE
|
|
v_address := NULL;
|
|
END IF;
|
|
|
|
v_phone := v_brand_settings.phone;
|
|
v_email := v_brand_settings.email;
|
|
v_website := v_brand_settings.website_url;
|
|
v_signature := v_brand_settings.default_email_signature;
|
|
|
|
-- Apply substitutions to subject
|
|
v_subj_final := v_campaign.subject;
|
|
v_subj_final := replace(v_subj_final, '{{company_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_subj_final := replace(v_subj_final, '{{brand_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_subj_final := replace(v_subj_final, '{{address}}', COALESCE(v_address, ''));
|
|
v_subj_final := replace(v_subj_final, '{{phone}}', COALESCE(v_phone, ''));
|
|
v_subj_final := replace(v_subj_final, '{{email}}', COALESCE(v_email, ''));
|
|
v_subj_final := replace(v_subj_final, '{{website}}', COALESCE(v_website, ''));
|
|
|
|
-- Apply substitutions to body, then append signature
|
|
v_body_final := v_campaign.body_text;
|
|
v_body_final := replace(v_body_final, '{{company_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_body_final := replace(v_body_final, '{{brand_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_body_final := replace(v_body_final, '{{address}}', COALESCE(v_address, ''));
|
|
v_body_final := replace(v_body_final, '{{phone}}', COALESCE(v_phone, ''));
|
|
v_body_final := replace(v_body_final, '{{email}}', COALESCE(v_email, ''));
|
|
v_body_final := replace(v_body_final, '{{website}}', COALESCE(v_website, ''));
|
|
|
|
-- Remove {{signature}} placeholder from body (replaced with actual signature below)
|
|
v_body_final := replace(v_body_final, '{{signature}}', '');
|
|
|
|
-- Append default email signature if set
|
|
IF v_signature IS NOT NULL AND v_signature != '' THEN
|
|
v_body_final := v_body_final || E'\n\n' || v_signature;
|
|
END IF;
|
|
|
|
SELECT * INTO v_settings
|
|
FROM communication_settings
|
|
WHERE brand_id = p_brand_id;
|
|
IF NOT FOUND THEN
|
|
RETURN jsonb_build_object('success', false, 'error', 'No communication settings for brand');
|
|
END IF;
|
|
|
|
v_audience := preview_campaign_audience(p_brand_id, v_campaign.audience_rules);
|
|
|
|
FOR v_customer IN
|
|
SELECT * FROM jsonb_array_elements(coalesce(v_audience->'sample_customers', '[]'::jsonb))
|
|
LOOP
|
|
v_entries := v_entries || jsonb_build_array(jsonb_build_object(
|
|
'brand_id', p_brand_id,
|
|
'campaign_id', p_campaign_id,
|
|
'customer_id', v_customer->>'id',
|
|
'customer_email', v_customer->>'email',
|
|
'delivery_method', 'email',
|
|
'subject', v_subj_final,
|
|
'body_preview', left(v_body_final, 500),
|
|
'status', 'queued'
|
|
));
|
|
v_count := v_count + 1;
|
|
END LOOP;
|
|
|
|
PERFORM log_communication_messages(v_entries);
|
|
|
|
UPDATE communication_campaigns
|
|
SET status = 'sent', sent_at = now(), updated_at = now()
|
|
WHERE id = p_campaign_id;
|
|
|
|
RETURN jsonb_build_object('success', true, 'messages_logged', v_count);
|
|
END;
|
|
$$;
|
|
|
|
-- ── Enhanced send_stop_blast ───────────────────────────────────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION public.send_stop_blast(
|
|
p_stop_id UUID,
|
|
p_brand_id UUID,
|
|
p_body TEXT,
|
|
p_channel TEXT DEFAULT 'email',
|
|
p_subject TEXT DEFAULT NULL,
|
|
p_audience TEXT DEFAULT 'pending',
|
|
p_created_by UUID DEFAULT NULL
|
|
)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_campaign_id UUID;
|
|
v_count INT := 0;
|
|
v_order RECORD;
|
|
v_contact JSONB;
|
|
v_customer_id UUID;
|
|
v_opted_in BOOLEAN;
|
|
v_email TEXT;
|
|
v_phone TEXT;
|
|
v_brand_name TEXT := 'Route Commerce';
|
|
v_signature TEXT;
|
|
v_address TEXT;
|
|
v_subj TEXT;
|
|
v_body_sub TEXT;
|
|
v_brand_settings RECORD;
|
|
BEGIN
|
|
-- Look up brand_settings for extended variables
|
|
SELECT
|
|
bs.legal_business_name,
|
|
bs.phone,
|
|
bs.email,
|
|
bs.website_url,
|
|
bs.default_email_signature,
|
|
bs.street_address,
|
|
bs.city,
|
|
bs.state,
|
|
bs.postal_code,
|
|
b.name AS brand_name
|
|
INTO v_brand_settings
|
|
FROM brand_settings bs
|
|
JOIN brands b ON b.id = p_brand_id
|
|
WHERE bs.brand_id = p_brand_id;
|
|
|
|
-- Build brand name: legal_business_name > brand_name > fallback
|
|
v_brand_name := COALESCE(
|
|
v_brand_settings.legal_business_name,
|
|
v_brand_settings.brand_name,
|
|
'Route Commerce'
|
|
);
|
|
|
|
-- Build full address string
|
|
IF v_brand_settings.street_address IS NOT NULL THEN
|
|
v_address := v_brand_settings.street_address
|
|
|| ', ' || COALESCE(v_brand_settings.city, '')
|
|
|| ', ' || COALESCE(v_brand_settings.state, '')
|
|
|| ' ' || COALESCE(v_brand_settings.postal_code, '');
|
|
v_address := trim(both ',' from v_address);
|
|
ELSE
|
|
v_address := NULL;
|
|
END IF;
|
|
|
|
v_signature := v_brand_settings.default_email_signature;
|
|
|
|
-- Apply substitutions to subject
|
|
v_subj := p_subject;
|
|
IF v_subj IS NOT NULL THEN
|
|
v_subj := replace(v_subj, '{{company_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_subj := replace(v_subj, '{{brand_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_subj := replace(v_subj, '{{address}}', COALESCE(v_address, ''));
|
|
v_subj := replace(v_subj, '{{phone}}', COALESCE(v_brand_settings.phone, ''));
|
|
v_subj := replace(v_subj, '{{email}}', COALESCE(v_brand_settings.email, ''));
|
|
v_subj := replace(v_subj, '{{website}}', COALESCE(v_brand_settings.website_url, ''));
|
|
END IF;
|
|
|
|
-- Apply substitutions to body, then append signature
|
|
v_body_sub := p_body;
|
|
v_body_sub := replace(v_body_sub, '{{company_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_body_sub := replace(v_body_sub, '{{brand_name}}', COALESCE(v_brand_name, 'Route Commerce'));
|
|
v_body_sub := replace(v_body_sub, '{{address}}', COALESCE(v_address, ''));
|
|
v_body_sub := replace(v_body_sub, '{{phone}}', COALESCE(v_brand_settings.phone, ''));
|
|
v_body_sub := replace(v_body_sub, '{{email}}', COALESCE(v_brand_settings.email, ''));
|
|
v_body_sub := replace(v_body_sub, '{{website}}', COALESCE(v_brand_settings.website_url, ''));
|
|
|
|
-- Append default email signature if set
|
|
IF v_signature IS NOT NULL AND v_signature != '' THEN
|
|
v_body_sub := v_body_sub || E'\n\n' || v_signature;
|
|
END IF;
|
|
|
|
-- Create campaign record
|
|
INSERT INTO communication_campaigns (
|
|
brand_id, name, subject, body_text,
|
|
campaign_type, status, audience_rules,
|
|
sent_at, created_by
|
|
) VALUES (
|
|
p_brand_id,
|
|
'Stop blast: ' || COALESCE(p_subject, p_body),
|
|
v_subj,
|
|
v_body_sub,
|
|
'operational',
|
|
'sent',
|
|
jsonb_build_object(
|
|
'target', 'stop',
|
|
'stop_id', p_stop_id,
|
|
'date_from', NULL,
|
|
'date_to', NULL
|
|
),
|
|
now(),
|
|
p_created_by
|
|
) RETURNING id INTO v_campaign_id;
|
|
|
|
-- Fetch matching orders
|
|
FOR v_order IN
|
|
SELECT o.id, o.customer_id, o.customer_name,
|
|
o.customer_email, o.customer_phone, o.pickup_complete,
|
|
c.email_opt_in, c.sms_opt_in, c.unsubscribed_at
|
|
FROM orders o
|
|
LEFT JOIN communication_contacts c ON c.id = o.customer_id
|
|
WHERE o.stop_id = p_stop_id
|
|
AND (
|
|
p_audience = 'all'
|
|
OR (p_audience = 'pending' AND NOT o.pickup_complete)
|
|
OR (p_audience = 'picked_up' AND o.pickup_complete)
|
|
)
|
|
LOOP
|
|
-- Check opt-in status per channel
|
|
v_opted_in := (
|
|
CASE p_channel
|
|
WHEN 'sms' THEN v_order.customer_phone IS NOT NULL
|
|
AND COALESCE(v_order.sms_opt_in, FALSE) = TRUE
|
|
AND v_order.unsubscribed_at IS NULL
|
|
WHEN 'email' THEN v_order.customer_email IS NOT NULL
|
|
AND COALESCE(v_order.email_opt_in, TRUE) = TRUE
|
|
AND v_order.unsubscribed_at IS NULL
|
|
ELSE v_order.customer_email IS NOT NULL
|
|
AND COALESCE(v_order.email_opt_in, TRUE) = TRUE
|
|
AND v_order.unsubscribed_at IS NULL
|
|
END
|
|
);
|
|
|
|
IF NOT v_opted_in THEN
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
-- Log email message with fully-substituted subject and body
|
|
IF p_channel IN ('email', 'both') AND v_order.customer_email IS NOT NULL THEN
|
|
INSERT INTO communication_message_logs (
|
|
brand_id, campaign_id, customer_id, customer_email,
|
|
delivery_method, subject, body_preview, status, sent_at
|
|
) VALUES (
|
|
p_brand_id, v_campaign_id, v_order.customer_id, v_order.customer_email,
|
|
'email', COALESCE(v_subj, ''), LEFT(v_body_sub, 200), 'queued', now()
|
|
);
|
|
v_count := v_count + 1;
|
|
END IF;
|
|
|
|
-- Log SMS message (signature not appended to SMS)
|
|
IF p_channel IN ('sms', 'both') AND v_order.customer_phone IS NOT NULL THEN
|
|
INSERT INTO communication_message_logs (
|
|
brand_id, campaign_id, customer_id, customer_phone,
|
|
delivery_method, body_preview, status, sent_at
|
|
) VALUES (
|
|
p_brand_id, v_campaign_id, v_order.customer_id, v_order.customer_phone,
|
|
'sms', LEFT(v_body_sub, 160), 'queued', now()
|
|
);
|
|
v_count := v_count + 1;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
RETURN jsonb_build_object(
|
|
'success', true,
|
|
'campaign_id', v_campaign_id,
|
|
'messages_logged', v_count
|
|
);
|
|
END;
|
|
$$; |