Skip to content

#62 — WhatsApp Business

PLANIFIÉ

Priorité: 🟠 HAUTE · Type: TYPE B · Conteneur: rgz-api · Code: app/services/whatsapp.pyDépendances: #1 rgz-api


Description

Integration Meta WhatsApp Business API pour notifications revendeurs et abonnés. Support messages template (incidents, SLA credits), webhook pour messages entrants, et auto-répondeur basique (solde, aide, déconnexion). Messages texte uniquement (pas d'images/vidéos pour MVP).

La plateforme WhatsApp offre un meilleur taux de livraison et de lecture que SMS pour les notifications critiques. Les revendeurs peuvent aussi envoyer des messages gratuitement via API une fois authentifiés.


Architecture Interne

Meta WhatsApp Cloud API Integration:

1. SETUP (une fois)
   - Business Account ID: créé dans Meta Business Manager
   - Phone Number ID: numéro WhatsApp Business (ex: +229-XXXXX)
   - Access Token: Bearer token avec scope messages_and_connections_messaging
   - Webhook URL: https://api-rgz.duckdns.org/api/v1/whatsapp/webhook
   - Webhook Verify Token: secret pour authentifier callbacks

2. FLOW ENVOI MESSAGE
   POST https://graph.instagram.com/v18.0/{PHONE_NUMBER_ID}/messages
   Header: Authorization: Bearer {ACCESS_TOKEN}
   Body: {
     messaging_product: "whatsapp",
     to: "+229-97979964",
     type: "template",
     template: {
       name: "incident_p0_alert",
       language: { code: "fr" },
       parameters: {
         body: {
           parameters: [
             { type: "text", text: "Fiber cut central" },
             { type: "text", text: "45" }
           ]
         }
       }
     }
   }
   Response: { messages: [{ id: "wamid.xxx" }] }

3. WEBHOOK CALLBACK (Meta → RGZ)
   POST /api/v1/whatsapp/webhook (inbound)
   Header: X-Hub-Signature-256: sha256=hmac(payload, WEBHOOK_SECRET)
   Body: {
     entry: [{
       changes: [{
         value: {
           messages: [{
             from: "+229-97979964",
             type: "text",
             text: { body: "solde" },
             timestamp: "1613060669",
             id: "wamid.xxx"
           }]
         }
       }]
     }]
   }
   → Parse message, trigger auto-response

4. AUTO-RÉPONDEUR (simple)
   Message reçu: "solde" → Répond: "Votre solde: X.XXX FCFA"
   Message reçu: "aide" → Répond: "Menu aide\n1. Solde\n2. Tickets\n3. Support\n4. Forfaits"
   Message reçu: "deconnexion" → Appelle CoA logout (#6 RADIUS)
   Autres messages: "Merci de contacter support@rgz.bj"

Modèles de Données

python
class WhatsAppMessage(BaseModel):
    id: UUID
    wamid: str  # Meta message ID
    recipient_phone: str (E164)
    message_type: str (template|text)
    template_name: str  # incident_p0_alert, sla_credit, etc
    message_content: str  # Final rendered text
    status: str (queued|sent|delivered|read|failed)
    created_at: datetime
    delivered_at: datetime | None
    read_at: datetime | None
    error_message: str | None

class WhatsAppInboundMessage(BaseModel):
    id: UUID
    wamid: str
    sender_phone: str (E164)
    message_type: str (text|image|video|document)
    content: str
    auto_response_sent: bool
    auto_response_text: str | None
    routed_to_support: bool
    created_at: datetime

# Table DB
whatsapp_messages:
  - id UUID PK
  - wamid VARCHAR(100) UNIQUE (Meta message ID)
  - recipient_phone VARCHAR(20)
  - message_type VARCHAR(20) CHECK(template|text)
  - template_name VARCHAR(100) NULL
  - content TEXT
  - status VARCHAR(20) CHECK(queued|sent|delivered|read|failed)
  - error_message TEXT NULL
  - created_at TIMESTAMP
  - delivered_at TIMESTAMP NULL
  - read_at TIMESTAMP NULL

whatsapp_inbound_messages:
  - id UUID PK
  - wamid VARCHAR(100) UNIQUE
  - sender_phone VARCHAR(20)
  - message_type VARCHAR(20)
  - content TEXT
  - auto_response_sent BOOLEAN DEFAULT false
  - auto_response_text TEXT NULL
  - routed_to_support BOOLEAN DEFAULT false
  - support_ticket_id UUID FK NULL
  - created_at TIMESTAMP

whatsapp_templates:
  - id UUID PK
  - name VARCHAR(100) UNIQUE
  - meta_template_name VARCHAR(100) UNIQUE (registered on Meta)
  - description TEXT
  - parameters_json TEXT (JSON schema of required parameters)
  - is_active BOOLEAN
  - created_at TIMESTAMP

Configuration

env
# .env.example
WHATSAPP_ACCESS_TOKEN=EAAxxxxxxxxxxxxxxxx
WHATSAPP_BUSINESS_ACCOUNT_ID=123456789
WHATSAPP_PHONE_NUMBER_ID=1023456789  # Business phone ID on Meta
WHATSAPP_WEBHOOK_SECRET=your_webhook_secret
WHATSAPP_WEBHOOK_VERIFY_TOKEN=your_verify_token
WHATSAPP_API_VERSION=v18.0
WHATSAPP_ENDPOINT=https://graph.instagram.com/v18.0

# Templates registered on Meta (managed separately)
WHATSAPP_TEMPLATE_INCIDENT_P0=incident_p0_alert
WHATSAPP_TEMPLATE_SLA_CREDIT=sla_credit
WHATSAPP_TEMPLATE_INVOICE=billing_invoice

WHATSAPP_AUTO_RESPONDER_ENABLED=true
WHATSAPP_SUPPORT_PHONE=+229-97979964  # Where to forward unknown msgs

Templates WhatsApp (Meta Dashboard)

Les templates doivent être enregistrés dans Meta Business Manager et approuvés :

TEMPLATE: incident_p0_alert
Category: MARKETING (ou TRANSACTIONAL si P0)
Language: French
Content:
  "🔴 Alerte RGZ P0: {{1}}
   {{2}} sites sans service
   Équipe mobilisée. MAJ dans {{3}}min
   Ticket: {{4}}"

Parameters:
  1. {{1}} = incident title (text)
  2. {{2}} = affected site count (text)
  3. {{3}} = escalation delay (text)
  4. {{4}} = incident number (text)

TEMPLATE: sla_credit
Category: TRANSACTIONAL
Language: French
Content:
  "Crédit RGZ appliqué ✓
   {{1}} FCFA
   Downtime {{2}}min (uptime {{3}}%)
   Merci!"

Parameters:
  1. {{1}} = amount FCFA
  2. {{2}} = downtime minutes
  3. {{3}} = uptime percent

TEMPLATE: billing_invoice
Category: TRANSACTIONAL
Language: French
Content:
  "Facture {{1}} prête
   {{2}} FCFA
   Télécharger: {{3}}"

Parameters:
  1. {{1}} = month
  2. {{2}} = amount FCFA
  3. {{3}} = portal link

Endpoints API

MéthodeRouteRequêteRéponseNotes
POST/api/v1/whatsapp/send{phone, template_name, parameters201 CREATED
POST/api/v1/whatsapp/webhook{entry:[...]} (Meta callback)200 OK
GET/api/v1/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=...200 OK (Meta verification)
GET/api/v1/whatsapp/messages?phone={number}&limit=50{items:[{wamid, content, status, created_at}], total}200 OK
GET/api/v1/whatsapp/templates{items:[{name, parameters}], total}200 OK

Commandes Utiles

bash
# Envoyer message template incident P0
curl -X POST http://localhost:8000/api/v1/whatsapp/send \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+229-97979964",
    "template_name": "incident_p0_alert",
    "parameters": {
      "title": "Fiber cut central",
      "affected_sites": "45",
      "delay": "5",
      "incident_number": "INC-2026-000847"
    },
    "message_type": "template"
  }'

# Envoyer SMS de crédit SLA
curl -X POST http://localhost:8000/api/v1/whatsapp/send \
  -d '{
    "phone": "+229-97979964",
    "template_name": "sla_credit",
    "parameters": {
      "amount_fcfa": "1000",
      "downtime_minutes": "15",
      "uptime_percent": "99.7"
    },
    "message_type": "template"
  }'

# Lister templates disponibles
curl http://localhost:8000/api/v1/whatsapp/templates

# Historique messages (NOC)
curl http://localhost:8000/api/v1/whatsapp/messages?phone=+229-97979964 \
  -H "Authorization: Bearer NOC_TOKEN" | jq '.items'

# Webhook callback Meta (reçu automatiquement)
# L'endpoint /api/v1/whatsapp/webhook parse inbound messages
# et déclenche auto-répondeur (solde, aide, etc)

Auto-Répondeur Logic

python
def auto_responder(message_content: str, sender_phone: str) -> str:
    """
    Répondre automatiquement aux messages simples
    """
    msg_lower = message_content.strip().lower()

    if msg_lower == "solde":
        balance = get_subscriber_balance(sender_phone)
        return f"Votre solde RGZ: {balance:.0f} FCFA 💰"

    elif msg_lower == "aide":
        return """Menu RGZ Assistant:
1. *solde* - Vérifier votre solde
2. *tickets* - Afficher mes tickets support
3. *support* - Contacter le support
4. *forfaits* - Voir mes forfaits actifs

Répondez avec le numéro ou le mot-clé.
Support: support@rgz.bj | +229-XXXXX"""

    elif msg_lower == "deconnexion":
        logout_subscriber(sender_phone)
        return "Déconnexion confirmée. À bientôt! 👋"

    elif msg_lower in ["tickets", "ticket"]:
        tickets = get_support_tickets(sender_phone)
        if not tickets:
            return "Vous n'avez pas de tickets ouverts."
        return f"Vos {len(tickets)} tickets ouverts:\n" + \
               "\n".join([f"• {t.number}: {t.title}" for t in tickets[:5]])

    elif msg_lower in ["forfaits", "forfait"]:
        forfaits = get_active_forfaits(sender_phone)
        if not forfaits:
            return "Aucun forfait actif. Visitez: https://access-rgz.duckdns.org"
        return f"Forfaits actifs ({len(forfaits)}):\n" + \
               "\n".join([f"• {f.name}: {f.status}" for f in forfaits])

    else:
        # Route to support if unknown
        return """Message non compris.
Merci de contacter le support: support@rgz.bj
ou appelez: +229-XXXXX"""

Intégration Avec Autres Outils

  • #58 incident-escalation: Appelle #62 pour alertes P0/P1/P2 (template incident_p0_alert, etc)
  • #25 credit-sla-auto: Appelle #62 pour notification crédit (template sla_credit)
  • #64 crisis-dispatcher: Appelle #62 pour broadcast crise
  • #61 sms-template-engine: Templates parallèles (SMS et WhatsApp même contenu)

Implémentation TODO

  • [ ] Meta WhatsApp API client (send message)
  • [ ] Webhook handler (inbound message parsing)
  • [ ] Auto-répondeur (solde, aide, tickets, forfaits, deconnexion)
  • [ ] Status callback handler (delivered, read, failed)
  • [ ] Error handling + retry logic
  • [ ] Message audit log (whatsapp_messages + whatsapp_inbound_messages)
  • [ ] Template registration (meta_template_name validation)
  • [ ] Rate limiting (via Redis rgz:rate:whatsapp:{phone})
  • [ ] Tests: envoi template → callback delivered → audit trail
  • [ ] Dashboard NOC: historique messages par revendeur
  • [ ] Support routing (unknown messages → support@rgz.bj)

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone