Summary
Ajouter un WhatsApp Gateway au système Unified Sender de Leadminer, permettant d'envoyer des campagnes WhatsApp via OpenWA. Le dialogue de création de campagne sera similaire à SMS, avec une différence majeure : pas de calcul de segments SMS.
Dépendance : #2826 (WhatsApp extraction via OpenWA) — partage l'infrastructure OpenWA (sessions, docker-compose, edge function whatsapp-fleet).
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Leadminer │
│ │
│ ┌────────────────────┐ ┌────────────────────────────────┐ │
│ │ Frontend │ │ Supabase Edge Functions │ │
│ │ │ │ │ │
│ │ WhatsAppCampaign │───▶│ whatsapp-campaigns │ │
│ │ ComposerDialog │ │ (création + preview) │ │
│ │ │ │ │ │
│ │ WhatsAppSession │───▶│ whatsapp-fleet │ │
│ │ QR Dialog │ │ (gateways + sessions OpenWA) │ │
│ │ │ │ │ │
│ │ SmsFleetManagement │ │ whatsapp-campaigns-process │ │
│ │ (extended) │ │ (envoi batch + fleet mgmt) │ │
│ └────────────────────┘ └──────────────┬─────────────────┘ │
│ │ │
│ Table: sms_fleet_gateways (provider='openwa') │
│ Tables: whatsapp_campaigns / _recipients │
└───────────────────────────────────────────┼───────────────────────┘
│ HTTP
┌───────┴───────┐
│ OpenWA │
│ (Docker) │
│ Sessions: │
│ - wa_123 │
│ - wa_456 │
└───────────────┘
Décisions clés
| Décision |
Choix |
| Channel |
Nouveau 'whatsapp' (séparé de 'sms') |
| Gateway management |
Réutiliser SmsFleetManagement (étendre provider list) |
| Provider interface |
Implémenter SmsProvider pour OpenWA |
| Session QR |
Interface dans Leadminer (wizard + polling) |
| Fleet mode v1 |
Multi-gateway (distribution des destinataires) |
| OpenWA dev |
Ajouter au docker-compose |
Phase 1 — Base de données
Migration : tables WhatsApp campaigns
-- Status enums
CREATE TYPE private.whatsapp_campaign_status AS ENUM (
'queued', 'processing', 'completed', 'failed', 'cancelled'
);
CREATE TYPE private.whatsapp_recipient_status AS ENUM (
'pending', 'sent', 'delivered', 'read', 'failed', 'skipped'
);
-- WhatsApp campaigns
CREATE TABLE private.whatsapp_campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
sender_name TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'fleet',
message_template TEXT NOT NULL,
footer_text_template TEXT,
use_short_links BOOLEAN DEFAULT false,
recipient_count INTEGER DEFAULT 0,
sent_count INTEGER DEFAULT 0,
delivered_count INTEGER DEFAULT 0,
read_count INTEGER DEFAULT 0,
failed_count INTEGER DEFAULT 0,
click_count INTEGER DEFAULT 0,
unsubscribe_count INTEGER DEFAULT 0,
status private.whatsapp_campaign_status DEFAULT 'queued',
fleet_mode_enabled BOOLEAN DEFAULT false,
selected_gateway_ids UUID[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
-- WhatsApp recipients
CREATE TABLE private.whatsapp_campaign_recipients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES private.whatsapp_campaigns(id) ON DELETE CASCADE,
phone TEXT NOT NULL,
message TEXT NOT NULL,
send_status private.whatsapp_recipient_status DEFAULT 'pending',
provider_message_id TEXT,
provider_error TEXT,
attempt_count INTEGER DEFAULT 0,
sent_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Recipient ↔ Gateway assignments (fleet mode)
CREATE TABLE private.whatsapp_campaign_recipient_gateways (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES private.whatsapp_campaigns(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL REFERENCES private.whatsapp_campaign_recipients(id) ON DELETE CASCADE,
gateway_id UUID REFERENCES private.sms_fleet_gateways(id) ON DELETE SET NULL,
gateway_name TEXT,
gateway_provider TEXT,
assigned_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(campaign_id, recipient_id)
);
-- Indexes + RLS (même pattern que sms_campaigns)
Migration : étendre sms_fleet_gateways provider
Pas de changement de schéma — le provider est en text, pas un enum. La valeur 'openwa' est acceptée directement.
Migration : étendre get_unified_campaigns_overview()
Ajouter le UNION ALL pour whatsapp_campaigns avec les colonnes delivered_count et read_count.
Phase 2 — Edge Functions Backend
2.1 whatsapp-fleet — Gestion gateways + sessions OpenWA
Fichier : supabase/functions/whatsapp-fleet/index.ts
Calqué sur sms-fleet/index.ts avec extensions :
- CRUD gateways :
GET/POST/PUT/DELETE /gateways (provider 'openwa')
- Sessions OpenWA (proxy vers OpenWA API) :
POST /sessions/create — Crée session OpenWA, retourne QR code
GET /sessions/:id/qr — Récupère QR code (pour polling frontend)
GET /sessions/:id/status — Statut session (connecting/connected/disconnected)
DELETE /sessions/:id — Supprime session OpenWA
2.2 whatsapp-campaigns — Gestion campagnes
Fichier : supabase/functions/whatsapp-campaigns/index.ts
Calqué sur sms-campaigns/index.ts avec :
- Endpoints :
POST /campaigns/create, GET /campaigns/list, POST /campaigns/preview
- Pas de calcul de segments SMS (différence clé)
- Template avec variables
{{name}}, {{email}}, etc.
- Footer avec unsubscribe
- Short links optionnel
- Sélection de gateways fleet (multi-select)
2.3 Provider OpenWA
Fichier : supabase/functions/whatsapp-campaigns/providers/openwa-provider.ts
class OpenWaProvider implements SmsProvider {
name = 'openwa';
constructor(private config: { baseUrl: string; sessionId: string }) {}
async send(params: SendSmsParams): Promise<SendSmsResult> {
// POST /api/send-message/{sessionId}
// Body: { chatId: params.to, text: params.body }
const res = await fetch(
`${this.config.baseUrl}/api/send-message/${this.config.sessionId}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatId: params.to, text: params.body }),
}
);
if (!res.ok) return { success: false, error: await res.text() };
const data = await res.json();
return { success: true, messageId: data.messageId };
}
}
Factory :
export function createWhatsAppProvider(
type: 'openwa',
options: { openwa: { baseUrl: string; sessionId: string } }
): SmsProvider
2.4 whatsapp-campaigns-process — Processor d'envoi
Fichier : supabase/functions/whatsapp-campaigns-process/index.ts
Calqué sur sms-campaigns-process/index.ts avec :
- Tables
whatsapp_campaigns / whatsapp_campaign_recipients
- Fleet mode : distribution des destinataires entre gateways OpenWA
- Création providers via config gateway (
baseUrl + sessionId)
- Gestion erreurs :
404 = numéro non-WhatsApp → failed permanent
429 = rate limit → retry avec backoff
- Session expirée → marquer gateway failed
- Statuts enrichis :
sent → delivered → read (polling ou webhook v2)
beforeunload handler pour sauvegarder la progression partielle
Phase 3 — Frontend
3.1 Types
// types/campaign.ts
type CampaignChannel = 'email' | 'sms' | 'whatsapp';
// types/sms-fleet.ts
type SmsGatewayProvider = 'smsgate' | 'simple-sms-gateway' | 'twilio' | 'openwa';
3.2 Nouveau composant WhatsAppCampaignComposerDialog.vue
Inspiré de SmsCampaignComposerDialog.vue :
- Sélecteur de gateway via
FleetGatewaySelector (filtré sur provider openwa)
- Template message avec variables (textarea)
- Pas de compteur/calcul de segments SMS
- Limite destinataires (InputNumber)
- Footer template avec unsubscribe
- Short links toggle
- Preview modal
3.3 Nouveau composant WhatsAppSessionDialog.vue
Wizard de création de session OpenWA :
- Saisir le nom de la session
POST /whatsapp-fleet/sessions/create
- Affichage du QR code (image base64)
- Polling du statut jusqu'à
connected
- Création automatique du gateway dans
sms_fleet_gateways
- Redirection vers configuration gateway
3.4 Extension de SmsFleetManagement.vue
- Ajouter
'openwa' dans la liste des providers
- Nouveau
ProviderForm pour OpenWA :
OpenWA Base URL (input)
Session ID (input, ou bouton "Connecter avec QR" → lance WhatsAppSessionDialog)
- Read-only : phone number associé
FleetGatewaySelector fonctionne déjà avec n'importe quel provider
3.5 Modification de pages/campaigns.vue
- Ajouter
'whatsapp' au type SenderFilter
- Tab "WhatsApp" dans
SenderFilterTabs
- Affichage campagnes WhatsApp : message preview, stats sent/delivered/read/failed, badge "OpenWA"
3.6 Store sms-fleet.ts
- Gère aussi les gateways
openwa (même CRUD)
- Pas de changement d'interface nécessaire
Phase 4 — Infrastructure
Docker Compose (dev)
services:
openwa:
image: openwa/openwa:latest
container_name: leadminer-openwa
ports:
- "3001:3000"
volumes:
- openwa_sessions:/app/sessions
environment:
- OPENWA_PORT=3000
- OPENWA_SESSION_NAME=leadminer
volumes:
openwa_sessions:
Variables d'environnement
OPENWA_BASE_URL — URL du service OpenWA
OPENWA_API_KEY — Clé API OpenWA (optionnel selon config)
Métriques WhatsApp vs SMS
| Métrique |
SMS |
WhatsApp |
| sent |
✅ |
✅ |
| delivered |
✅ |
✅ |
| read |
❌ |
✅ (read receipts) |
| failed |
✅ |
✅ |
| click |
❌ |
✅ (même système) |
| unsubscribe |
✅ |
✅ |
| segment calculation |
✅ |
❌ |
Points de vigilance
- QR Code session : Timeout de 60s pour le scan. Polling frontend → backend → OpenWA.
- Session expirée : WhatsApp Web sessions expire. Détecter + proposer reconnexion.
- Rate limits : ~1 msg/sec par session. Throttle dans le processor.
- Format numéro : E.164 obligatoire pour WhatsApp.
- CGU : whatsapp-web.js = non-officiel. Documenter le risque de ban.
- Pas de SMS segments : 1 message WhatsApp = 1 message. Pas de calcul.
Répartition des fichiers
supabase/functions/
├── whatsapp-fleet/
│ └── index.ts # CRUD gateways + sessions OpenWA
├── whatsapp-campaigns/
│ ├── index.ts # Création campagnes WhatsApp
│ ├── providers/
│ │ ├── types.ts # Reuse SmsProvider interface
│ │ ├── mod.ts # createWhatsAppProvider()
│ │ └── openwa-provider.ts
│ └── utils/
│ ├── phone.ts # Validation E.164
│ └── short-link.ts # Share with sms-campaigns
└── whatsapp-campaigns-process/
└── index.ts # Envoi batch + fleet
supabase/migrations/
└── YYYYMMDDHHMMSS_add_whatsapp_campaigns.sql
frontend/src/
├── components/
│ ├── whatsapp-fleet/
│ │ └── WhatsAppSessionDialog.vue
│ └── campaigns/
│ └── WhatsAppCampaignComposerDialog.vue
├── pages/
│ └── campaigns.vue # (modifié: + filtre WhatsApp)
├── types/
│ ├── campaign.ts # (modifié: + whatsapp channel)
│ └── sms-fleet.ts # (modifié: + openwa provider)
└── stores/
└── sms-fleet.ts # (modifié: gère aussi openwa)
Estimation
| # |
Phase |
Estim. |
| 1 |
Migration SQL (tables + overview) |
1 j |
| 2a |
whatsapp-fleet edge function (CRUD + sessions) |
1.5 j |
| 2b |
whatsapp-campaigns edge function |
1.5 j |
| 2c |
OpenWA provider + factory |
0.5 j |
| 2d |
whatsapp-campaigns-process (batch + fleet) |
2 j |
| 3a |
WhatsAppCampaignComposerDialog |
1.5 j |
| 3b |
WhatsAppSessionDialog (QR wizard) |
1.5 j |
| 3c |
Extension SmsFleetManagement + ProviderForm |
1 j |
| 3d |
Modification campaigns.vue + SenderFilterTabs |
0.5 j |
| 4 |
Docker compose dev |
0.5 j |
| 5 |
Tests + QA |
1.5 j |
| Total |
|
~13 j-dev |
Dépendances
Références
- OpenWA GitHub
sms-campaigns/index.ts — Pattern à suivre
sms-campaigns-process/index.ts — Pattern fleet mode
sms-fleet/index.ts — Pattern CRUD gateways
SmsCampaignComposerDialog.vue — Pattern composer dialog
Summary
Ajouter un WhatsApp Gateway au système Unified Sender de Leadminer, permettant d'envoyer des campagnes WhatsApp via OpenWA. Le dialogue de création de campagne sera similaire à SMS, avec une différence majeure : pas de calcul de segments SMS.
Dépendance : #2826 (WhatsApp extraction via OpenWA) — partage l'infrastructure OpenWA (sessions, docker-compose, edge function
whatsapp-fleet).Architecture
Décisions clés
'whatsapp'(séparé de'sms')SmsFleetManagement(étendre provider list)SmsProviderpour OpenWAPhase 1 — Base de données
Migration : tables WhatsApp campaigns
Migration : étendre
sms_fleet_gatewaysproviderPas de changement de schéma — le provider est en
text, pas un enum. La valeur'openwa'est acceptée directement.Migration : étendre
get_unified_campaigns_overview()Ajouter le
UNION ALLpourwhatsapp_campaignsavec les colonnesdelivered_countetread_count.Phase 2 — Edge Functions Backend
2.1
whatsapp-fleet— Gestion gateways + sessions OpenWAFichier :
supabase/functions/whatsapp-fleet/index.tsCalqué sur
sms-fleet/index.tsavec extensions :GET/POST/PUT/DELETE /gateways(provider'openwa')POST /sessions/create— Crée session OpenWA, retourne QR codeGET /sessions/:id/qr— Récupère QR code (pour polling frontend)GET /sessions/:id/status— Statut session (connecting/connected/disconnected)DELETE /sessions/:id— Supprime session OpenWA2.2
whatsapp-campaigns— Gestion campagnesFichier :
supabase/functions/whatsapp-campaigns/index.tsCalqué sur
sms-campaigns/index.tsavec :POST /campaigns/create,GET /campaigns/list,POST /campaigns/preview{{name}},{{email}}, etc.2.3 Provider OpenWA
Fichier :
supabase/functions/whatsapp-campaigns/providers/openwa-provider.tsFactory :
2.4
whatsapp-campaigns-process— Processor d'envoiFichier :
supabase/functions/whatsapp-campaigns-process/index.tsCalqué sur
sms-campaigns-process/index.tsavec :whatsapp_campaigns/whatsapp_campaign_recipientsbaseUrl+sessionId)404= numéro non-WhatsApp → failed permanent429= rate limit → retry avec backoffsent→delivered→read(polling ou webhook v2)beforeunloadhandler pour sauvegarder la progression partiellePhase 3 — Frontend
3.1 Types
3.2 Nouveau composant
WhatsAppCampaignComposerDialog.vueInspiré de
SmsCampaignComposerDialog.vue:FleetGatewaySelector(filtré sur provideropenwa)3.3 Nouveau composant
WhatsAppSessionDialog.vueWizard de création de session OpenWA :
POST /whatsapp-fleet/sessions/createconnectedsms_fleet_gateways3.4 Extension de
SmsFleetManagement.vue'openwa'dans la liste des providersProviderFormpour OpenWA :OpenWA Base URL(input)Session ID(input, ou bouton "Connecter avec QR" → lanceWhatsAppSessionDialog)FleetGatewaySelectorfonctionne déjà avec n'importe quel provider3.5 Modification de
pages/campaigns.vue'whatsapp'au typeSenderFilterSenderFilterTabs3.6 Store
sms-fleet.tsopenwa(même CRUD)Phase 4 — Infrastructure
Docker Compose (dev)
Variables d'environnement
OPENWA_BASE_URL— URL du service OpenWAOPENWA_API_KEY— Clé API OpenWA (optionnel selon config)Métriques WhatsApp vs SMS
Points de vigilance
Répartition des fichiers
Estimation
Dépendances
Références
sms-campaigns/index.ts— Pattern à suivresms-campaigns-process/index.ts— Pattern fleet modesms-fleet/index.ts— Pattern CRUD gatewaysSmsCampaignComposerDialog.vue— Pattern composer dialog