fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
-- Migration 081: Stop blast via communication_campaigns
|
||||
-- Routes stop-based blast messages through the campaign system for analytics
|
||||
-- and to respect contact opt-in/opt-out preferences.
|
||||
|
||||
CREATE OR REPLACE FUNCTION send_stop_blast(
|
||||
p_stop_id UUID,
|
||||
p_brand_id UUID,
|
||||
p_body TEXT,
|
||||
p_channel TEXT DEFAULT 'email', -- 'sms', 'email', 'both'
|
||||
p_subject TEXT DEFAULT NULL,
|
||||
p_audience TEXT DEFAULT 'pending', -- 'all', 'pending', 'picked_up'
|
||||
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_subj TEXT;
|
||||
v_body_sub TEXT;
|
||||
BEGIN
|
||||
-- Resolve brand name for {{company_name}} substitution
|
||||
SELECT b.name INTO v_brand_name
|
||||
FROM brands b
|
||||
WHERE b.id = p_brand_id;
|
||||
IF v_brand_name IS NULL THEN
|
||||
SELECT invoice_business_name INTO v_brand_name
|
||||
FROM wholesale_settings
|
||||
WHERE brand_id = p_brand_id
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
IF v_brand_name IS NULL THEN
|
||||
v_brand_name := 'Route Commerce';
|
||||
END IF;
|
||||
|
||||
v_subj := replace(replace(p_subject, '{{company_name}}', v_brand_name), '{{brand_name}}', v_brand_name);
|
||||
v_body_sub := replace(replace(p_body, '{{company_name}}', v_brand_name), '{{brand_name}}', v_brand_name);
|
||||
|
||||
-- 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
|
||||
-- Default to opted-in for email (email_opt_in defaults TRUE in schema)
|
||||
-- Default to opted-OUT for SMS (sms_opt_in defaults FALSE in schema)
|
||||
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
|
||||
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', v_subj, LEFT(v_body_sub, 200), 'queued', now()
|
||||
);
|
||||
v_count := v_count + 1;
|
||||
END IF;
|
||||
|
||||
-- Log SMS message
|
||||
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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user