Files
bakery-ia/services/notification/MULTI_TENANT_WHATSAPP_IMPLEMENTATION.md

18 KiB

Multi-Tenant WhatsApp Configuration Implementation

Overview

This document describes the implementation of per-tenant WhatsApp Business phone number configuration, allowing each bakery to use their own WhatsApp Business account for sending notifications to suppliers.


Problem Statement

Before: All tenants shared a single global WhatsApp Business account configured via environment variables.

After: Each tenant can configure their own WhatsApp Business account in their bakery settings, with credentials stored securely in the database.


Architecture

Data Flow

Bakery Settings UI (Frontend)
        ↓
Tenant Settings API
        ↓
tenant_settings.notification_settings (Database)
        ↓
Notification Service fetches tenant settings
        ↓
WhatsAppBusinessService uses tenant-specific credentials
        ↓
Meta WhatsApp Cloud API

###Tenant Isolation

Each tenant has:

  • Own WhatsApp Phone Number ID
  • Own WhatsApp Access Token
  • Own WhatsApp Business Account ID
  • Independent enable/disable toggle

Implementation Progress

Phase 1: Backend - Tenant Service (COMPLETED)

1.1 Database Model

File: services/tenant/app/models/tenant_settings.py

Changes:

  • Added notification_settings JSON column to TenantSettings model
  • Includes WhatsApp and Email configuration
  • Default values set for new tenants

Fields Added:

notification_settings = {
    # WhatsApp Configuration
    "whatsapp_enabled": False,
    "whatsapp_phone_number_id": "",
    "whatsapp_access_token": "",
    "whatsapp_business_account_id": "",
    "whatsapp_api_version": "v18.0",
    "whatsapp_default_language": "es",

    # Email Configuration
    "email_enabled": True,
    "email_from_address": "",
    "email_from_name": "",
    "email_reply_to": "",

    # Notification Preferences
    "enable_po_notifications": True,
    "enable_inventory_alerts": True,
    "enable_production_alerts": True,
    "enable_forecast_alerts": True,

    # Notification Channels
    "po_notification_channels": ["email"],
    "inventory_alert_channels": ["email"],
    "production_alert_channels": ["email"],
    "forecast_alert_channels": ["email"]
}

1.2 Pydantic Schema

File: services/tenant/app/schemas/tenant_settings.py

Changes:

  • Created NotificationSettings Pydantic schema
  • Added validation for required fields when WhatsApp is enabled
  • Added to TenantSettingsResponse and TenantSettingsUpdate

Validators:

  • Validates channels are valid (email, whatsapp, sms, push)
  • Requires whatsapp_phone_number_id when WhatsApp enabled
  • Requires whatsapp_access_token when WhatsApp enabled

1.3 Service Layer

File: services/tenant/app/services/tenant_settings_service.py

Changes:

  • Added NotificationSettings to imports
  • Added "notification" to CATEGORY_SCHEMAS map
  • Added "notification": "notification_settings" to CATEGORY_COLUMNS map

Effect: The service now automatically handles notification settings through existing methods:

  • get_category(tenant_id, "notification")
  • update_category(tenant_id, "notification", updates)
  • reset_category(tenant_id, "notification")

1.4 Database Migration

File: services/tenant/migrations/versions/002_add_notification_settings.py

Purpose: Adds notification_settings column to existing tenant_settings table

Migration Details:

  • Adds JSONB column with default values
  • All existing tenants get default notification settings
  • Reversible (downgrade removes column)

To Run:

cd services/tenant
alembic upgrade head

🔄 Phase 2: Backend - Notification Service (IN PROGRESS)

This phase needs to be completed. Here's what needs to be done:

2.1 Add Tenant Client Dependency

File: services/notification/app/core/config.py or dependency injection

Action: Add TenantServiceClient to notification service

Code needed:

from shared.clients.tenant_client import TenantServiceClient

# In service initialization or dependency
tenant_client = TenantServiceClient(config)

2.2 Modify WhatsAppBusinessService

File: services/notification/app/services/whatsapp_business_service.py

Changes needed:

  1. Accept tenant_id as parameter
  2. Fetch tenant notification settings
  3. Use tenant-specific credentials if available
  4. Fall back to global config if tenant settings empty

Example Implementation:

async def send_message(self, request: SendWhatsAppMessageRequest, tenant_client: TenantServiceClient):
    # Fetch tenant settings
    settings = await tenant_client.get_notification_settings(request.tenant_id)

    # Use tenant-specific credentials if WhatsApp enabled
    if settings.get("whatsapp_enabled"):
        access_token = settings.get("whatsapp_access_token")
        phone_number_id = settings.get("whatsapp_phone_number_id")
        business_account_id = settings.get("whatsapp_business_account_id")
    else:
        # Fall back to global config
        access_token = self.access_token
        phone_number_id = self.phone_number_id
        business_account_id = self.business_account_id

    # Send message using selected credentials
    ...

2.3 Update PO Event Consumer

File: services/notification/app/consumers/po_event_consumer.py

Changes needed:

  1. Inject TenantServiceClient
  2. Fetch tenant settings before sending WhatsApp
  3. Check if WhatsApp is enabled for tenant
  4. Use appropriate channels

Example:

# In __init__
self.tenant_client = tenant_client

# In send_po_approved_whatsapp
async def send_po_approved_whatsapp(self, event_data):
    tenant_id = event_data.get('data', {}).get('tenant_id')

    # Get tenant notification settings
    settings = await self.tenant_client.get_notification_settings(tenant_id)

    # Check if WhatsApp enabled
    if not settings.get("whatsapp_enabled"):
        logger.info("WhatsApp not enabled for tenant", tenant_id=tenant_id)
        return False

    # Check if PO notifications include WhatsApp channel
    channels = settings.get("po_notification_channels", [])
    if "whatsapp" not in channels:
        logger.info("WhatsApp not in PO notification channels", tenant_id=tenant_id)
        return False

    # Send WhatsApp (service will use tenant-specific credentials)
    ...

📋 Phase 3: Frontend - Settings UI (PENDING)

3.1 TypeScript Types

File: frontend/src/api/types/settings.ts

Add:

export interface NotificationSettings {
  // WhatsApp Configuration
  whatsapp_enabled: boolean;
  whatsapp_phone_number_id: string;
  whatsapp_access_token: string;
  whatsapp_business_account_id: string;
  whatsapp_api_version: string;
  whatsapp_default_language: string;

  // Email Configuration
  email_enabled: boolean;
  email_from_address: string;
  email_from_name: string;
  email_reply_to: string;

  // Notification Preferences
  enable_po_notifications: boolean;
  enable_inventory_alerts: boolean;
  enable_production_alerts: boolean;
  enable_forecast_alerts: boolean;

  // Notification Channels
  po_notification_channels: string[];
  inventory_alert_channels: string[];
  production_alert_channels: string[];
  forecast_alert_channels: string[];
}

export interface BakerySettings {
  ...existing fields...
  notification_settings: NotificationSettings;
}

3.2 Settings Page - Add Tab

File: frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx

Add new tab:

<TabsTrigger value="notifications">
  <Bell className="w-4 h-4 mr-2" />
  {t('bakery.tabs.notifications')}
</TabsTrigger>

<TabsContent value="notifications">
  <NotificationSettingsCard settings={settings} onUpdate={handleUpdate} />
</TabsContent>

3.3 Notification Settings Card Component

File: frontend/src/components/settings/NotificationSettingsCard.tsx (new file)

Features:

  • Toggle for WhatsApp enabled/disabled
  • Input fields for WhatsApp credentials (Phone Number ID, Access Token, Business Account ID)
  • Password-style input for Access Token
  • Test Connection button
  • Email configuration fields
  • Channel selection checkboxes for each notification type

Example Structure:

export function NotificationSettingsCard({ settings, onUpdate }: Props) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{t('notifications.whatsapp_config')}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* WhatsApp Enable Toggle */}
        <Switch
          checked={settings.whatsapp_enabled}
          onCheckedChange={(enabled) => onUpdate({ whatsapp_enabled: enabled })}
        />

        {/* WhatsApp Credentials (shown when enabled) */}
        {settings.whatsapp_enabled && (
          <>
            <Input
              label={t('notifications.phone_number_id')}
              value={settings.whatsapp_phone_number_id}
              onChange={(e) => onUpdate({ whatsapp_phone_number_id: e.target.value })}
            />

            <Input
              type="password"
              label={t('notifications.access_token')}
              value={settings.whatsapp_access_token}
              onChange={(e) => onUpdate({ whatsapp_access_token: e.target.value })}
            />

            <Button onClick={testConnection}>
              {t('notifications.test_connection')}
            </Button>
          </>
        )}

        {/* Channel Selection */}
        <div>
          <Label>{t('notifications.po_channels')}</Label>
          <Checkbox
            checked={settings.po_notification_channels.includes('email')}
            label="Email"
          />
          <Checkbox
            checked={settings.po_notification_channels.includes('whatsapp')}
            label="WhatsApp"
            disabled={!settings.whatsapp_enabled}
          />
        </div>
      </CardContent>
    </Card>
  );
}

3.4 i18n Translations

Files:

  • frontend/src/locales/es/settings.json
  • frontend/src/locales/eu/settings.json

Add translations:

{
  "notifications": {
    "title": "Notificaciones",
    "whatsapp_config": "Configuración de WhatsApp",
    "whatsapp_enabled": "Activar WhatsApp",
    "phone_number_id": "ID de Número de Teléfono",
    "access_token": "Token de Acceso",
    "business_account_id": "ID de Cuenta de Negocio",
    "test_connection": "Probar Conexión",
    "email_config": "Configuración de Email",
    "po_channels": "Canales para Órdenes de Compra",
    "inventory_channels": "Canales para Alertas de Inventario",
    "test_success": "Conexión exitosa",
    "test_failed": "Error en la conexión",
    "save_success": "Configuración guardada",
    "save_error": "Error al guardar"
  }
}

Testing Guide

Backend Testing

1. Test Tenant Settings API

# Get notification settings
curl -X GET "http://localhost:8001/api/v1/tenants/{tenant_id}/settings/category/notification" \
  -H "Authorization: Bearer {token}"

# Update notification settings
curl -X PUT "http://localhost:8001/api/v1/tenants/{tenant_id}/settings/category/notification" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "whatsapp_enabled": true,
      "whatsapp_phone_number_id": "123456789",
      "whatsapp_access_token": "EAAxxxx",
      "whatsapp_business_account_id": "987654321",
      "po_notification_channels": ["email", "whatsapp"]
    }
  }'

2. Test WhatsApp Message with Tenant Config

# Send test PO notification (should use tenant's WhatsApp config)
# Trigger a PO approval and check logs:
kubectl logs -f deployment/notification-service | grep "WhatsApp"

# Should see logs indicating tenant-specific credentials being used

3. Verify Database

-- Check notification settings for all tenants
SELECT
  tenant_id,
  notification_settings->>'whatsapp_enabled' as whatsapp_enabled,
  notification_settings->>'whatsapp_phone_number_id' as phone_id
FROM tenant_settings;

-- Check WhatsApp messages sent
SELECT
  tenant_id,
  recipient_phone,
  status,
  template_name,
  created_at
FROM whatsapp_messages
ORDER BY created_at DESC
LIMIT 10;

Frontend Testing

  1. Navigate to Settings:

    • Go to Bakery Settings page
    • Click on "Notifications" tab
  2. Configure WhatsApp:

    • Toggle WhatsApp enabled
    • Enter WhatsApp credentials from Meta Business Suite
    • Click "Test Connection" button
    • Should see success message if credentials valid
  3. Configure Channels:

    • Enable WhatsApp for PO notifications
    • Save settings
    • Verify settings persist after page reload
  4. Test End-to-End:

    • Configure WhatsApp for a tenant
    • Create and approve a purchase order
    • Verify WhatsApp message sent to supplier
    • Check message appears in WhatsApp messages table

Security Considerations

⚠️ Access Token Storage

Current: Access tokens stored as plain text in JSON field

Recommended for Production:

  1. Encrypt access tokens before storing
  2. Use field-level encryption
  3. Decrypt only when needed

Implementation:

from cryptography.fernet import Fernet

class EncryptionService:
    def encrypt(self, value: str) -> str:
        # Encrypt using Fernet or AWS KMS
        pass

    def decrypt(self, encrypted_value: str) -> str:
        # Decrypt
        pass

# In tenant settings service
encrypted_token = encryption_service.encrypt(access_token)
settings["whatsapp_access_token"] = encrypted_token

Role-Based Access Control

Only owners and admins should be able to:

  • View WhatsApp credentials
  • Update notification settings
  • Test WhatsApp connection

Implementation: Add role check in API endpoint

@router.put("/api/v1/tenants/{tenant_id}/settings/category/notification")
async def update_notification_settings(
    tenant_id: UUID,
    settings: CategoryUpdateRequest,
    current_user: User = Depends(get_current_user)
):
    # Check role
    if current_user.role not in ["owner", "admin"]:
        raise HTTPException(status_code=403, detail="Insufficient permissions")

    # Update settings
    ...

Migration Guide

For Existing Tenants

When the migration runs, all existing tenants will get default notification settings with WhatsApp disabled.

To enable WhatsApp for existing tenants:

  1. Get WhatsApp Business API credentials from Meta Business Suite
  2. Update tenant settings via API or UI
  3. Test configuration using test endpoint
  4. Enable WhatsApp for desired notification types

From Global to Per-Tenant

If you have a global WhatsApp configuration you want to migrate:

# Migration script (run once)
async def migrate_global_to_tenant():
    # Get global config
    global_phone_id = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
    global_token = os.getenv("WHATSAPP_ACCESS_TOKEN")
    global_account_id = os.getenv("WHATSAPP_BUSINESS_ACCOUNT_ID")

    # Update all tenant settings
    tenants = await get_all_tenants()
    for tenant in tenants:
        settings = {
            "whatsapp_enabled": True,
            "whatsapp_phone_number_id": global_phone_id,
            "whatsapp_access_token": global_token,
            "whatsapp_business_account_id": global_account_id
        }
        await update_tenant_notification_settings(tenant.id, settings)

Deployment Steps

1. Backend Deployment

# 1. Deploy tenant service with new schema
cd services/tenant
alembic upgrade head

# 2. Deploy notification service with updated code
kubectl apply -f kubernetes/notification-deployment.yaml

# 3. Verify migration
kubectl exec -it deployment/tenant-service -- alembic current

# 4. Check logs
kubectl logs -f deployment/notification-service | grep "notification_settings"

2. Frontend Deployment

cd frontend
npm run build
# Deploy built frontend

3. Verification

  • Check tenant settings API responds with notification_settings
  • Verify frontend shows Notifications tab
  • Test WhatsApp configuration for one tenant
  • Send test PO notification

Troubleshooting

Issue: "notification_settings not found in database"

Cause: Migration not run

Solution:

cd services/tenant
alembic upgrade head

Issue: "WhatsApp still using global config"

Cause: Notification service not updated to fetch tenant settings

Solution: Complete Phase 2 implementation (see above)

Issue: "Access token validation fails"

Cause: Invalid or expired token

Solution:

  1. Generate new permanent token from Meta Business Suite
  2. Update tenant settings with new token
  3. Test connection

Next Steps

  1. Complete Phase 2: Update notification service to fetch and use tenant settings
  2. Complete Phase 3: Build frontend UI for configuration
  3. Add Encryption: Implement field-level encryption for access tokens
  4. Add Audit Logging: Log all changes to notification settings
  5. Add Test Endpoint: Create endpoint to test WhatsApp connection
  6. Update Documentation: Add tenant-specific setup to WhatsApp setup guide

Files Modified

Backend - Tenant Service

  • services/tenant/app/models/tenant_settings.py
  • services/tenant/app/schemas/tenant_settings.py
  • services/tenant/app/services/tenant_settings_service.py
  • services/tenant/migrations/versions/002_add_notification_settings.py

Backend - Notification Service (Pending)

  • services/notification/app/services/whatsapp_business_service.py
  • services/notification/app/consumers/po_event_consumer.py
  • services/notification/app/core/config.py or DI setup

Frontend (Pending)

  • frontend/src/api/types/settings.ts
  • frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx
  • frontend/src/components/settings/NotificationSettingsCard.tsx (new)
  • frontend/src/locales/es/settings.json
  • frontend/src/locales/eu/settings.json

Summary

Completed (Phase 1):

  • Database schema for per-tenant notification settings
  • Pydantic validation schemas
  • Service layer support
  • Database migration

Remaining:

  • Notification service integration with tenant settings
  • Frontend UI for configuration
  • Security enhancements (encryption, RBAC)
  • Testing and documentation updates

This implementation provides a solid foundation for multi-tenant WhatsApp configuration. Each bakery can now configure their own WhatsApp Business account, with credentials stored securely and settings easily manageable through the UI.