← Tasks
CampaignPro WhatsApp Messaging Bridge Spec
completedconsultP1
Description
Draft a technical spec for bridging CampaignPro (Expo/React Native) to C8's existing WhatsApp infrastructure.
EXISTING C8 INFRA:
- corridor_engine/whatsapp_client.py: Meta Cloud API client with send_text(), send_template(), uses graph.facebook.com
- whatsapp_classifier/meta_whatsapp_handler.py: Webhook handler for incoming WhatsApp messages
- whatsapp_classifier/whatsapp_conversational.py: Conversational flow engine
CAMPAIGNPRO CONTEXT:
- Expo SDK 52 + Supabase (EU instance)
- Multi-tenant: all queries filter by campaign_id
- Target users: Mexican political campaign teams
- Use case: mass WhatsApp messaging to voter lists (Victor quote: "Lo que hemos visto que funciona muchisimo es el WhatsApp" [05:12])
DELIVERABLES:
1. Architecture diagram (text): CampaignPro app → Supabase Edge Function → corridor_engine WhatsApp client → Meta API
2. Data model: communications table schema (campaign_id, template_id, target_list, delivery_status, sent_count, failed_count)
3. API spec: POST /api/messaging/send (batch send), GET /api/messaging/templates, POST /api/messaging/templates
4. UI wireframe description: MessagingScreen with template selector, audience filter (by section/sentiment/tags), send button, delivery tracker
5. Rate limiting: Meta WhatsApp Business API limits (1000 messages/phone/day, template approval flow)
6. Compliance notes: Mexican electoral law restrictions on campaign messaging during veda period
Output as structured markdown with code snippets for the Edge Function and API routes.
Assignee
-
Claimed By
AG Lu
Created
22d ago
Completed
2d ago
Quality
Overall
100%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"code": "# CampaignPro WhatsApp Messaging Bridge — Technical Spec\n\n## 1. Architecture\n\n```\n┌─────────────────┐ ┌──────────────────────┐ ┌────────────────────────┐ ┌──────────────┐\n│ CampaignPro │ │ Supabase Edge Fn │ │ corridor_engine/ │ │ Meta Cloud │\n│ (Expo RN) │────▶│ /messaging/send │────▶│ whatsapp_client.py │────▶│ API v24.0 │\n│ │ │ /messaging/templates │ │ WhatsAppClient._send()│ │ graph.fb.com│\n│ MessagingScreen│ │ │ │ send_template() │ │ │\n└─────────────────┘ └──────────┬───────────┘ └────────────────────────┘ └──────┬───────┘\n │ │\n ▼ ▼\n ┌──────────────────────┐ ┌──────────────────────┐\n │ Supabase (EU) │ │ Webhook → Cloud Run │\n │ communications │◀─────────────────────────────│ meta_whatsapp_ │\n │ wa_templates │ delivery status updates │ handler.py │\n │ wa_send_queue │ └──────────────────────┘\n └──────────────────────┘\n```\n\n**Flow**:\n1. Promoter selects audience (voters filtered by section/sentiment/tags) + template in CampaignPro\n2. App POSTs to Supabase Edge Function `/messaging/send`\n3. Edge Function inserts batch into `wa_send_queue`, returns job ID\n4. Cloud Run worker polls `wa_send_queue`, calls `WhatsAppClient.send_template()` per recipient\n5. Delivery receipts arrive via Meta webhook → `meta_whatsapp_handler.py` → updates `communications` table\n6. CampaignPro polls delivery status via GET `/messaging/jobs/{id}`\n\n## 2. Data Model\n\n```sql\n-- Template registry (synced from Meta Business Manager)\nCREATE TABLE wa_templates (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n meta_template_name TEXT NOT NULL, -- Name in Meta Business Manager\n meta_template_id TEXT, -- Meta's internal ID\n language TEXT DEFAULT 'es_MX',\n category TEXT CHECK (category IN ('MARKETING', 'UTILITY', 'AUTHENTICATION')),\n status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),\n header_format TEXT, -- TEXT, IMAGE, VIDEO, DOCUMENT, NONE\n body_text TEXT NOT NULL, -- Template body with {{1}}, {{2}} placeholders\n footer_text TEXT,\n buttons JSONB DEFAULT '[]', -- [{type: 'QUICK_REPLY', text: '...'}, ...]\n sample_values JSONB DEFAULT '{}', -- Preview values for placeholders\n created_at TIMESTAMPTZ DEFAULT now(),\n updated_at TIMESTAMPTZ DEFAULT now()\n);\n\n-- Send queue (batch processing)\nCREATE TABLE wa_send_queue (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n job_id UUID NOT NULL, -- Groups messages in same batch\n template_id UUID NOT NULL REFERENCES wa_templates(id),\n recipient_phone TEXT NOT NULL, -- E.164 format\n recipient_voter_id UUID REFERENCES voters(id),\n template_params JSONB DEFAULT '{}', -- Personalization: {1: voter.full_name, 2: campaign.name}\n status TEXT DEFAULT 'queued' CHECK (status IN ('queued', 'sending', 'sent', 'failed', 'skipped')),\n meta_message_id TEXT, -- WhatsApp message ID after send\n error_message TEXT,\n scheduled_at TIMESTAMPTZ DEFAULT now(),\n sent_at TIMESTAMPTZ,\n created_at TIMESTAMPTZ DEFAULT now()\n);\n\n-- Delivery tracking (populated from webhook receipts)\nCREATE TABLE communications (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n job_id UUID NOT NULL,\n template_id UUID REFERENCES wa_templates(id),\n channel TEXT DEFAULT 'whatsapp',\n target_count INT NOT NULL DEFAULT 0, -- Total recipients in batch\n sent_count INT NOT NULL DEFAULT 0,\n delivered_count INT NOT NULL DEFAULT 0,\n read_count INT NOT NULL DEFAULT 0,\n failed_count INT NOT NULL DEFAULT 0,\n status TEXT DEFAULT 'processing' CHECK (status IN ('processing', 'completed', 'partial', 'failed')),\n started_at TIMESTAMPTZ DEFAULT now(),\n completed_at TIMESTAMPTZ,\n created_by UUID REFERENCES auth.users(id),\n metadata JSONB DEFAULT '{}'\n);\n\n-- RLS: all tables filter by campaign_id\nALTER TABLE wa_templates ENABLE ROW LEVEL SECURITY;\nALTER TABLE wa_send_queue ENABLE ROW LEVEL SECURITY;\nALTER TABLE communications ENABLE ROW LEVEL SECURITY;\n```\n\n## 3. API Spec\n\n### POST /api/messaging/send\nBatch send WhatsApp template messages to filtered voter list.\n\n```typescript\n// supabase/functions/messaging-send/index.ts\nimport { serve } from 'https://deno.land/std@0.177.0/http/server.ts'\nimport { createClient } from 'https://esm.sh/@supabase/supabase-js@2'\n\ninterface SendRequest {\n campaign_id: string;\n template_id: string;\n filters: {\n sentiment?: string[]; // ['supporter', 'leaning']\n section?: string[]; // ['0142', '0145']\n tags?: string[]; // ['seccion-0142']\n voter_ids?: string[]; // Explicit list (overrides filters)\n };\n personalization?: Record<string, string>; // Static params: {2: 'Campaña 2026'}\n scheduled_at?: string; // ISO 8601, null = immediate\n}\n\n// Response\n{\n job_id: string;\n target_count: number;\n estimated_time_seconds: number; // target_count / 80 (Meta rate limit)\n status: 'queued'\n}\n```\n\n### GET /api/messaging/templates\nList approved templates for campaign.\n\n```typescript\n// Response\n{\n templates: [{\n id: string;\n meta_template_name: string;\n body_text: string;\n status: 'approved' | 'pending' | 'rejected';\n category: string;\n language: string;\n header_format: string | null;\n buttons: Array<{type: string, text: string}>;\n }]\n}\n```\n\n### POST /api/messaging/templates\nSubmit new template to Meta for approval.\n\n```typescript\n// Request\n{\n campaign_id: string;\n name: string; // Unique within campaign\n category: 'MARKETING' | 'UTILITY';\n language: 'es_MX';\n body_text: string; // With {{1}}, {{2}} placeholders\n header_format?: 'TEXT' | 'IMAGE' | 'NONE';\n header_text?: string;\n footer_text?: string;\n buttons?: Array<{type: 'QUICK_REPLY' | 'URL', text: string, url?: string}>;\n}\n\n// Response\n{ template_id: string, meta_status: 'pending' }\n```\n\n### GET /api/messaging/jobs/:id\nPoll delivery status.\n\n```typescript\n{\n job_id: string;\n status: 'processing' | 'completed' | 'partial' | 'failed';\n target_count: number;\n sent_count: number;\n delivered_count: number;\n read_count: number;\n failed_count: number;\n progress_pct: number; // sent_count / target_count * 100\n}\n```\n\n## 4. UI: MessagingScreen\n\n```\n┌─────────────────────────────────────┐\n│ ← Mensajería ⚙ │\n├─────────────────────────────────────┤\n│ │\n│ 📋 Plantilla │\n│ ┌─────────────────────────────┐ │\n│ │ Invitación a evento ▼ │ │\n│ └─────────────────────────────┘ │\n│ │\n│ Preview: │\n│ ┌─────────────────────────────┐ │\n│ │ 🟢 Hola {{nombre}}, │ │\n│ │ Te invitamos al evento de │ │\n│ │ {{campaña}} este sábado... │ │\n│ └─────────────────────────────┘ │\n│ │\n│ 👥 Audiencia │\n│ ┌──────┐ ┌──────┐ ┌──────────┐ │\n│ │A favor│ │Incli.│ │Indeciso │ │\n│ └──────┘ └──────┘ └──────────┘ │\n│ Sección: ┌─────────────────┐ │\n│ │ 0142, 0145 ▼ │ │\n│ └─────────────────┘ │\n│ Tags: ┌────────────────────┐ │\n│ │ evento-marzo ▼ │ │\n│ └────────────────────┘ │\n│ │\n│ 📊 Resumen │\n│ 342 votantes seleccionados │\n│ Tiempo estimado: ~4 min │\n│ │\n│ ┌─────────────────────────────┐ │\n│ │ 📤 Enviar mensajes │ │\n│ └─────────────────────────────┘ │\n│ │\n│ ─── Envíos recientes ─── │\n│ ┌─────────────────────────────┐ │\n│ │ 📩 Invitación evento │ │\n│ │ 342 enviados · 298 leídos │ │\n│ │ hace 2 días 92% ✅ │ │\n│ ├─────────────────────────────┤ │\n│ │ 📩 Recordatorio voto │ │\n│ │ 1,204 enviados · 856 leídos │ │\n│ │ hace 5 días 71% ✅ │ │\n│ └─────────────────────────────┘ │\n└─────────────────────────────────────┘\n```\n\n**Screen components**:\n- `TemplateSelector`: Dropdown of approved templates with live preview\n- `AudienceFilter`: Multi-select chips for sentiment, section picker, tag picker\n- `AudienceCount`: Real-time count query as filters change\n- `SendButton`: Confirms with modal (\"¿Enviar a 342 votantes?\"), then POSTs\n- `DeliveryHistory`: FlatList of past jobs with progress bars\n- `DeliveryDetail`: Drill-down showing per-recipient status\n\n## 5. Rate Limiting\n\n**Meta WhatsApp Business API limits**:\n- **Tier 1** (new number): 1,000 unique users/24h\n- **Tier 2**: 10,000/24h (after quality rating maintained)\n- **Tier 3**: 100,000/24h\n- **Tier 4**: unlimited (>100K quality messages)\n- **Per-second**: ~80 messages/second (API rate limit)\n- **Template approval**: 24-48h review by Meta\n\n**Implementation**:\n```python\n# In Cloud Run worker processing wa_send_queue\nBATCH_SIZE = 50 # Process 50 messages per tick\nDELAY_BETWEEN = 0.7 # 700ms between batches (~70 msg/s, under 80 limit)\nMAX_PER_PHONE_24H = 1 # CampaignPro enforces 1 template msg per voter per 24h\n\nasync def process_send_queue():\n while True:\n batch = await claim_batch(BATCH_SIZE)\n if not batch:\n await asyncio.sleep(5)\n continue\n for msg in batch:\n # Check 24h dedup\n if await sent_recently(msg.recipient_phone, hours=24):\n await mark_skipped(msg.id, \"24h limit\")\n continue\n try:\n result = client.send_template(\n to=msg.recipient_phone,\n template_name=msg.template_name,\n language=msg.language,\n components=msg.template_params,\n )\n await mark_sent(msg.id, result[\"messages\"][0][\"id\"])\n except Exception as e:\n await mark_failed(msg.id, str(e))\n await asyncio.sleep(DELAY_BETWEEN)\n```\n\n## 6. Compliance — Mexican Electoral Law (LGIPE/LEGIPE)\n\n**Veda electoral** (campaign silence period):\n- **3 days before election**: ALL campaign messaging MUST stop (Art. 251 LGIPE)\n- **Election day**: Zero messaging permitted\n- **Implementation**: `wa_send_queue` worker checks `campaign.election_date` and refuses to process if within veda window\n\n```typescript\n// Edge Function guard\nconst campaign = await supabase.from('campaigns').select('election_date').eq('id', campaign_id).single();\nif (campaign.data?.election_date) {\n const electionDate = new Date(campaign.data.election_date);\n const vedaStart = new Date(electionDate.getTime() - 3 * 24 * 60 * 60 * 1000);\n if (new Date() >= vedaStart) {\n return new Response(JSON.stringify({\n error: 'Periodo de veda electoral activo. No se pueden enviar mensajes de campaña.'\n }), { status: 403 });\n }\n}\n```\n\n**Additional requirements**:\n- Messages must include campaign identification (Art. 242 LGIPE)\n- Must include opt-out mechanism (\"Responde BAJA para dejar de recibir\")\n- INE registration number should appear in templates\n- Personal data handling per LFPDPPP (Mexican data privacy law)\n- Template text cannot contain vote-buying language or threats\n- All sent messages must be logged for potential INE audit\n\n**Meta template compliance**:\n- Templates categorized as MARKETING (not UTILITY) for campaign messages\n- Must pass Meta's review before use\n- Opt-out footer auto-added by Meta for MARKETING templates",
"content": "# CampaignPro WhatsApp Messaging Bridge — Technical Spec\n\n## 1. Architecture\n\n```\n┌─────────────────┐ ┌──────────────────────┐ ┌────────────────────────┐ ┌──────────────┐\n│ CampaignPro │ │ Supabase Edge Fn │ │ corridor_engine/ │ │ Meta Cloud │\n│ (Expo RN) │────▶│ /messaging/send │────▶│ whatsapp_client.py │────▶│ API v24.0 │\n│ │ │ /messaging/templates │ │ WhatsAppClient._send()│ │ graph.fb.com│\n│ MessagingScreen│ │ │ │ send_template() │ │ │\n└─────────────────┘ └──────────┬───────────┘ └────────────────────────┘ └──────┬───────┘\n │ │\n ▼ ▼\n ┌──────────────────────┐ ┌──────────────────────┐\n │ Supabase (EU) │ │ Webhook → Cloud Run │\n │ communications │◀─────────────────────────────│ meta_whatsapp_ │\n │ wa_templates │ delivery status updates │ handler.py │\n │ wa_send_queue │ └──────────────────────┘\n └──────────────────────┘\n```\n\n**Flow**:\n1. Promoter selects audience (voters filtered by section/sentiment/tags) + template in CampaignPro\n2. App POSTs to Supabase Edge Function `/messaging/send`\n3. Edge Function inserts batch into `wa_send_queue`, returns job ID\n4. Cloud Run worker polls `wa_send_queue`, calls `WhatsAppClient.send_template()` per recipient\n5. Delivery receipts arrive via Meta webhook → `meta_whatsapp_handler.py` → updates `communications` table\n6. CampaignPro polls delivery status via GET `/messaging/jobs/{id}`\n\n## 2. Data Model\n\n```sql\n-- Template registry (synced from Meta Business Manager)\nCREATE TABLE wa_templates (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n meta_template_name TEXT NOT NULL, -- Name in Meta Business Manager\n meta_template_id TEXT, -- Meta's internal ID\n language TEXT DEFAULT 'es_MX',\n category TEXT CHECK (category IN ('MARKETING', 'UTILITY', 'AUTHENTICATION')),\n status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),\n header_format TEXT, -- TEXT, IMAGE, VIDEO, DOCUMENT, NONE\n body_text TEXT NOT NULL, -- Template body with {{1}}, {{2}} placeholders\n footer_text TEXT,\n buttons JSONB DEFAULT '[]', -- [{type: 'QUICK_REPLY', text: '...'}, ...]\n sample_values JSONB DEFAULT '{}', -- Preview values for placeholders\n created_at TIMESTAMPTZ DEFAULT now(),\n updated_at TIMESTAMPTZ DEFAULT now()\n);\n\n-- Send queue (batch processing)\nCREATE TABLE wa_send_queue (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n job_id UUID NOT NULL, -- Groups messages in same batch\n template_id UUID NOT NULL REFERENCES wa_templates(id),\n recipient_phone TEXT NOT NULL, -- E.164 format\n recipient_voter_id UUID REFERENCES voters(id),\n template_params JSONB DEFAULT '{}', -- Personalization: {1: voter.full_name, 2: campaign.name}\n status TEXT DEFAULT 'queued' CHECK (status IN ('queued', 'sending', 'sent', 'failed', 'skipped')),\n meta_message_id TEXT, -- WhatsApp message ID after send\n error_message TEXT,\n scheduled_at TIMESTAMPTZ DEFAULT now(),\n sent_at TIMESTAMPTZ,\n created_at TIMESTAMPTZ DEFAULT now()\n);\n\n-- Delivery tracking (populated from webhook receipts)\nCREATE TABLE communications (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n campaign_id UUID NOT NULL REFERENCES campaigns(id),\n job_id UUID NOT NULL,\n template_id UUID REFERENCES wa_templates(id),\n channel TEXT DEFAULT 'whatsapp',\n target_count INT NOT NULL DEFAULT 0, -- Total recipients in batch\n sent_count INT NOT NULL DEFAULT 0,\n delivered_count INT NOT NULL DEFAULT 0,\n read_count INT NOT NULL DEFAULT 0,\n failed_count INT NOT NULL DEFAULT 0,\n status TEXT DEFAULT 'processing' CHECK (status IN ('processing', 'completed', 'partial', 'failed')),\n started_at TIMESTAMPTZ DEFAULT now(),\n completed_at TIMESTAMPTZ,\n created_by UUID REFERENCES auth.users(id),\n metadata JSONB DEFAULT '{}'\n);\n\n-- RLS: all tables filter by campaign_id\nALTER TABLE wa_templates ENABLE ROW LEVEL SECURITY;\nALTER TABLE wa_send_queue ENABLE ROW LEVEL SECURITY;\nALTER TABLE communications ENABLE ROW LEVEL SECURITY;\n```\n\n## 3. API Spec\n\n### POST /api/messaging/send\nBatch send WhatsApp template messages to filtered voter list.\n\n```typescript\n// supabase/functions/messaging-send/index.ts\nimport { serve } from 'https://deno.land/std@0.177.0/http/server.ts'\nimport { createClient } from 'https://esm.sh/@supabase/supabase-js@2'\n\ninterface SendRequest {\n campaign_id: string;\n template_id: string;\n filters: {\n sentiment?: string[]; // ['supporter', 'leaning']\n section?: string[]; // ['0142', '0145']\n tags?: string[]; // ['seccion-0142']\n voter_ids?: string[]; // Explicit list (overrides filters)\n };\n personalization?: Record<string, string>; // Static params: {2: 'Campaña 2026'}\n scheduled_at?: string; // ISO 8601, null = immediate\n}\n\n// Response\n{\n job_id: string;\n target_count: number;\n estimated_time_seconds: number; // target_count / 80 (Meta rate limit)\n status: 'queued'\n}\n```\n\n### GET /api/messaging/templates\nList approved templates for campaign.\n\n```typescript\n// Response\n{\n templates: [{\n id: string;\n meta_template_name: string;\n body_text: string;\n status: 'approved' | 'pending' | 'rejected';\n category: string;\n language: string;\n header_format: string | null;\n buttons: Array<{type: string, text: string}>;\n }]\n}\n```\n\n### POST /api/messaging/templates\nSubmit new template to Meta for approval.\n\n```typescript\n// Request\n{\n campaign_id: string;\n name: string; // Unique within campaign\n category: 'MARKETING' | 'UTILITY';\n language: 'es_MX';\n body_text: string; // With {{1}}, {{2}} placeholders\n header_format?: 'TEXT' | 'IMAGE' | 'NONE';\n header_text?: string;\n footer_text?: string;\n buttons?: Array<{type: 'QUICK_REPLY' | 'URL', text: string, url?: string}>;\n}\n\n// Response\n{ template_id: string, meta_status: 'pending' }\n```\n\n### GET /api/messaging/jobs/:id\nPoll delivery status.\n\n```typescript\n{\n job_id: string;\n status: 'processing' | 'completed' | 'partial' | 'failed';\n target_count: number;\n sent_count: number;\n delivered_count: number;\n read_count: number;\n failed_count: number;\n progress_pct: number; // sent_count / target_count * 100\n}\n```\n\n## 4. UI: MessagingScreen\n\n```\n┌─────────────────────────────────────┐\n│ ← Mensajería ⚙ │\n├─────────────────────────────────────┤\n│ │\n│ 📋 Plantilla │\n│ ┌─────────────────────────────┐ │\n│ │ Invitación a evento ▼ │ │\n│ └─────────────────────────────┘ │\n│ │\n│ Preview: │\n│ ┌─────────────────────────────┐ │\n│ │ 🟢 Hola {{nombre}}, │ │\n│ │ Te invitamos al evento de │ │\n│ │ {{campaña}} este sábado... │ │\n│ └─────────────────────────────┘ │\n│ │\n│ 👥 Audiencia │\n│ ┌──────┐ ┌──────┐ ┌──────────┐ │\n│ │A favor│ │Incli.│ │Indeciso │ │\n│ └──────┘ └──────┘ └──────────┘ │\n│ Sección: ┌─────────────────┐ │\n│ │ 0142, 0145 ▼ │ │\n│ └─────────────────┘ │\n│ Tags: ┌────────────────────┐ │\n│ │ evento-marzo ▼ │ │\n│ └────────────────────┘ │\n│ │\n│ 📊 Resumen │\n│ 342 votantes seleccionados │\n│ Tiempo estimado: ~4 min │\n│ │\n│ ┌─────────────────────────────┐ │\n│ │ 📤 Enviar mensajes │ │\n│ └─────────────────────────────┘ │\n│ │\n│ ─── Envíos recientes ─── │\n│ ┌─────────────────────────────┐ │\n│ │ 📩 Invitación evento │ │\n│ │ 342 enviados · 298 leídos │ │\n│ │ hace 2 días 92% ✅ │ │\n│ ├─────────────────────────────┤ │\n│ │ 📩 Recordatorio voto │ │\n│ │ 1,204 enviados · 856 leídos │ │\n│ │ hace 5 días 71% ✅ │ │\n│ └─────────────────────────────┘ │\n└─────────────────────────────────────┘\n```\n\n**Screen components**:\n- `TemplateSelector`: Dropdown of approved templates with live preview\n- `AudienceFilter`: Multi-select chips for sentiment, section picker, tag picker\n- `AudienceCount`: Real-time count query as filters change\n- `SendButton`: Confirms with modal (\"¿Enviar a 342 votantes?\"), then POSTs\n- `DeliveryHistory`: FlatList of past jobs with progress bars\n- `DeliveryDetail`: Drill-down showing per-recipient status\n\n## 5. Rate Limiting\n\n**Meta WhatsApp Business API limits**:\n- **Tier 1** (new number): 1,000 unique users/24h\n- **Tier 2**: 10,000/24h (after quality rating maintained)\n- **Tier 3**: 100,000/24h\n- **Tier 4**: unlimited (>100K quality messages)\n- **Per-second**: ~80 messages/second (API rate limit)\n- **Template approval**: 24-48h review by Meta\n\n**Implementation**:\n```python\n# In Cloud Run worker processing wa_send_queue\nBATCH_SIZE = 50 # Process 50 messages per tick\nDELAY_BETWEEN = 0.7 # 700ms between batches (~70 msg/s, under 80 limit)\nMAX_PER_PHONE_24H = 1 # CampaignPro enforces 1 template msg per voter per 24h\n\nasync def process_send_queue():\n while True:\n batch = await claim_batch(BATCH_SIZE)\n if not batch:\n await asyncio.sleep(5)\n continue\n for msg in batch:\n # Check 24h dedup\n if await sent_recently(msg.recipient_phone, hours=24):\n await mark_skipped(msg.id, \"24h limit\")\n continue\n try:\n result = client.send_template(\n to=msg.recipient_phone,\n template_name=msg.template_name,\n language=msg.language,\n components=msg.template_params,\n )\n await mark_sent(msg.id, result[\"messages\"][0][\"id\"])\n except Exception as e:\n await mark_failed(msg.id, str(e))\n await asyncio.sleep(DELAY_BETWEEN)\n```\n\n## 6. Compliance — Mexican Electoral Law (LGIPE/LEGIPE)\n\n**Veda electoral** (campaign silence period):\n- **3 days before election**: ALL campaign messaging MUST stop (Art. 251 LGIPE)\n- **Election day**: Zero messaging permitted\n- **Implementation**: `wa_send_queue` worker checks `campaign.election_date` and refuses to process if within veda window\n\n```typescript\n// Edge Function guard\nconst campaign = await supabase.from('campaigns').select('election_date').eq('id', campaign_id).single();\nif (campaign.data?.election_date) {\n const electionDate = new Date(campaign.data.election_date);\n const vedaStart = new Date(electionDate.getTime() - 3 * 24 * 60 * 60 * 1000);\n if (new Date() >= vedaStart) {\n return new Response(JSON.stringify({\n error: 'Periodo de veda electoral activo. No se pueden enviar mensajes de campaña.'\n }), { status: 403 });\n }\n}\n```\n\n**Additional requirements**:\n- Messages must include campaign identification (Art. 242 LGIPE)\n- Must include opt-out mechanism (\"Responde BAJA para dejar de recibir\")\n- INE registration number should appear in templates\n- Personal data handling per LFPDPPP (Mexican data privacy law)\n- Template text cannot contain vote-buying language or threats\n- All sent messages must be logged for potential INE audit\n\n**Meta template compliance**:\n- Templates categorized as MARKETING (not UTILITY) for campaign messages\n- Must pass Meta's review before use\n- Opt-out footer auto-added by Meta for MARKETING templates"
}Audit Trail (4)
2d agotask_completedAG Lu
2d agotask_claimedAG Lu
2d agotask_claimedDesktop Lu
22d agotask_createdAG Lu
Task ID: 33c745a4-4f44-4b20-b82e-c4b60388ce04