# Mawidi WhatsApp Feature — Complete Reference

> Branch: `feature/whatsapp` · 38 commits ahead of `master`
> Last updated: 2026-04-08

---

## What Was Built

A fully multi-tenant, sector-aware, bilingual (Arabic/English) WhatsApp AI platform built on top of Twilio, with Meta Cloud API as an alternative provider. The system handles the entire journey: inbound message → language detection → AI engine response → Qatari dialect post-processing → outbound reply → dashboard real-time view.

---

## Architecture Overview

```
Customer WhatsApp
        │
        ▼
Twilio (or Meta Cloud API)
        │
        ▼
POST /api/webhooks/twilio-whatsapp         ← main Twilio webhook (all sectors)
POST /api/webhooks/twilio-whatsapp-re      ← real-estate Twilio webhook
POST /api/integrations/whatsapp/webhook    ← Meta Cloud API webhook
        │
        ▼
whatsapp-router.service.ts                 ← resolves org from phone/WABA ID
        │
        ├── Language detection + preference lookup (whatsapp-language.service.ts)
        ├── First-message bilingual greeting (whatsapp-greeting.service.ts)
        ├── Language selection handler (1=Arabic / 2=English)
        ├── Human handoff check (30 min timer)
        │
        ▼
whatsapp-ai-engine.service.ts              ← sector-aware AI engine
        │
        ├── sector-registry.ts             ← loads SectorConfig for org's industry
        ├── whatsapp-knowledge-base.service.ts ← RAG lookup (Gemini FileSearch)
        ├── whatsapp-availability.service.ts   ← real-time calendar availability
        ├── whatsapp-payment-links.service.ts  ← Stripe deposit links
        │
        ▼
qatari-dialect.service.ts                  ← post-process AI response to Gulf Arabic
        │
        ▼
twilio-whatsapp.service.ts                 ← send outbound message
        │
        ▼
Convex: org_whatsapp_conversations + org_whatsapp_messages
        │
        ▼
Dashboard: WhatsAppInbox → ConversationDetailModal (5-second auto-poll)
```

---

## Database Schema (Convex)

File: `convex/schema/whatsapp.ts`

### `org_whatsapp_conversations`
| Field | Type | Notes |
|---|---|---|
| `organizationId` | string | Owning org — all queries scoped to this |
| `userPhone` | string | E.164 format |
| `userName` | string? | WhatsApp display name |
| `language` | string | `"en"` or `"ar"` — resolved after selection |
| `status` | string | `"active"` / `"resolved"` / `"escalated"` |
| `conversationState` | JSON | `BookingFlowState` — current phase of booking |
| `messageCount` | number | |
| `lastMessageAt` | Date | Updated on each turn |
| `humanControlled` | bool | Set when staff takes over |
| `humanControlledAt` | Date? | |
| `humanControlledBy` | string? | userId of staff member |
| `customerEmail` | string? | Captured during booking flow |

### `org_whatsapp_messages`
| Field | Type | Notes |
|---|---|---|
| `conversationId` | string | FK to `org_whatsapp_conversations` |
| `role` | string | `"user"` / `"assistant"` / `"staff"` |
| `content` | string | Message text |
| `language` | string | `"en"` or `"ar"` per message |
| `intent` | string? | AI-classified intent |
| `toolsUsed` | string[] | AI tools invoked for this turn |
| `kbUsed` | bool | Was Knowledge Base consulted |
| `kbConfidence` | number | RAG confidence 0–1 |
| `escalatedToHuman` | bool | AI decided to escalate |
| `metadata` | JSON? | e.g. `{ twilioMessageSid, sentBy }` for staff messages |
| `timestamp` | Date | |

### `user_language_preferences`
| Field | Type | Notes |
|---|---|---|
| `organizationId` | string | |
| `userPhone` | string | Normalized E.164 |
| `language` | string | `"en"` / `"ar"` |
| `selectedAt` | Date | Last updated |

Unique index: `organizationId_userPhone`

### Organization fields for WhatsApp (on `organizations` table)
| Field | Purpose |
|---|---|
| `twilioAccountSid` | Twilio account ID |
| `twilioAuthTokenEncrypted` | AES-encrypted auth token |
| `twilioWhatsappNumber` | e.g. `+14155238886` |
| `whatsappPhoneNumberId` | Meta Cloud API phone number ID |
| `industry` | Maps to sector for AI engine |
| `greetingMessageEn` / `greetingMessageAr` | Custom welcome messages |
| `autoGreetingEnabled` | `false` = skip first-message greeting |

---

## API Routes

### Inbound Webhooks

| Route | Provider | Notes |
|---|---|---|
| `POST /api/webhooks/twilio-whatsapp` | Twilio | All sectors — main entry point |
| `POST /api/webhooks/twilio-whatsapp-re` | Twilio | Real-estate sector specific |
| `POST /api/integrations/whatsapp/webhook` | Meta Cloud API | GET for webhook verification |

### Meta OAuth / Connect

| Route | Purpose |
|---|---|
| `GET /api/integrations/whatsapp/oauth/state` | Generate CSRF state token |
| `GET /api/integrations/whatsapp/oauth/callback` | Complete Meta Embedded Signup OAuth |
| `GET /api/integrations/whatsapp/status` | Check connection status |
| `POST /api/integrations/whatsapp/connect` | Manual connect with token |
| `POST /api/integrations/whatsapp/disconnect` | Disconnect Meta |

### Dashboard

| Route | Purpose |
|---|---|
| `GET /api/dashboard/whatsapp-conversations` | List org conversations (paginated, filtered) |
| `GET /api/dashboard/whatsapp-conversations/[id]` | Single conversation + 200 messages (5s poll) |
| `POST /api/dashboard/whatsapp-conversations/[id]/reply` | Staff manual reply → flips humanControlled |
| `POST /api/dashboard/whatsapp-conversations/[id]/handoff` | Toggle humanControlled on/off |
| `GET/POST /api/dashboard/whatsapp-greeting` | Read/update org greeting messages |
| `GET/POST /api/dashboard/suggested-faqs` | AI-suggested FAQs from conversation learning |

---

## Core Services

### `lib/services/twilio-whatsapp.service.ts`
- `getCredentialsForOrg(orgId)` — fetches + decrypts Twilio creds from org record
- `validateSignature(authToken, sig, url, params)` — Twilio HMAC signature check
- `sendTextMessage(credentials, to, body)` — outbound send; adds `whatsapp:` prefix if needed
- `parseIncomingMessage(params)` — parses Twilio form POST into `TwilioIncomingMessage`
- `lookupOrgByNumber(phone)` — find org from Twilio WhatsApp number

### `lib/services/whatsapp-router.service.ts`
Multi-tenant router. Resolves incoming `To` number → organization record.
- Checks `org.twilioWhatsappNumber` index in Convex
- Caches org lookups to avoid repeated DB hits per message

### `lib/services/whatsapp-ai-engine.service.ts`
The main AI brain. Entry: `processMessage(context, body, sectorConfig)`.

Flow:
1. Builds system prompt from `SectorConfig.systemPromptTemplate`
2. Calls Knowledge Base RAG if KB query confidence > threshold
3. Calls availability calendar if booking-related intent detected
4. Generates booking/viewing records via `BookingFlowState` state machine
5. Returns `EngineResult { responseText, intent, newState, kbUsed, kbConfidence, toolsUsed }`

`BookingFlowState` phases:
```
idle → collecting_name → collecting_contact → collecting_datetime →
collecting_property (RE only) → confirming → confirmed
```

### `lib/services/whatsapp-language.service.ts`
- `getLanguagePreference(orgId, phone)` — Redis-backed lookup with DB fallback
- `setLanguagePreference(orgId, phone, lang)` — upsert to `user_language_preferences`
- Language detection regex: `/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/` — Arabic if >30% of chars

### `lib/services/whatsapp-greeting.service.ts`
- `buildGreetingForOrg(orgId, orgName)` — returns `{ text, requiresLanguageSelection }`
- Custom greeting: shows org's AR + EN custom messages with language picker appended
- Default: bilingual `هلا 👋` / `Hello 👋` + `1️⃣ العربية / 2️⃣ English`
- `autoGreetingEnabled: false` on org = return `{ text: "" }` = skip greeting

### `lib/services/qatari-dialect.service.ts`
Post-processes AI Arabic responses to authentic Gulf Arabic.

Three-tier translation:
1. **Common phrases** — 400+ pre-computed phrase map (instant, 0ms)
2. **LRU cache** — 2000 entries, MD5-keyed
3. **Anthropic API** — `claude-haiku-4-5-20251001` with Qatari-flavored system prompt

Key rules enforced:
- Never use `أبيك` (reads as "your father") → use `تبي`
- Never use `ولا` for "or" → use `أو`
- Never use Levantine `شو` → use Gulf `شنو`
- Preserve URLs intact (extracted before translation, re-appended after)

File: `lib/services/qatari-dialect.service.ts`  
Vocabulary: `lib/data/qatari-vocabulary.json`

### `lib/services/whatsapp-knowledge-base.service.ts`
RAG layer. Queries Gemini FileSearch against uploaded org documents.
- Used when AI detects KB-relevant intent (pricing, services, policies)
- Returns confidence score — included in message record as `kbUsed` / `kbConfidence`

### `lib/services/whatsapp-payment-links.service.ts`
Generates Stripe payment links for deposits.
- Only activates if org has `STRIPE_SECRET_KEY` configured
- Creates one-time payment intents with booking metadata

### `lib/services/conversation-learning.service.ts`
Non-blocking post-turn analysis. Called with `void analyzeConversationTurn(...)`.
Tracks: question → intent → KB used → KB confidence → escalated.
Feeds into suggested FAQ generation (see below).

### `lib/services/conversation-cleanup.service.ts`
Marks stale conversations as resolved after configurable inactivity period.

---

## Sector System

### `lib/config/sector-registry.ts`
Maps `org.industry` string → `SectorConfig`.

`SectorConfig` shape:
```ts
{
  id: string;
  transactionNoun: { en: string; ar: string };    // "booking" / "حجز"
  systemPromptTemplate: (ctx) => string;          // AI system prompt builder
  collectFields: string[];                         // e.g. ["name","phone","datetime"]
  bookingConfirmTemplate: (info) => string;
  customFields?: Record<string, SectorFieldConfig>;
}
```

Supported sectors (via `lib/config/sector-default-workflows.ts`):
healthcare, beauty_wellness, food_hospitality, retail_services, property_facilities, mobility_automotive, home_services_trades, education, events_venues, pet_services, public_sector, real-estate (special path)

### Real Estate Sector
Special path via `/api/webhooks/twilio-whatsapp-re`. Extra capabilities:
- Injects live property listings into AI system prompt (`feat(whatsapp): inject live property listings`)
- Captures `REViewingInfo` (property address, unit, preferred datetime)
- Saves viewings to `viewings` Convex table + calendar sync
- `lib/services/real-estate/whatsapp-templates.service.ts` — formatted property cards

---

## Dashboard Components

### `app/[lang]/dashboard/components/WhatsAppInbox.tsx`
- Lists `org_whatsapp_conversations` via `GET /api/dashboard/whatsapp-conversations`
- Filters: language, status, phone/name search
- Groups by date
- Opens `ConversationDetailModal` on row click

### `components/dashboard/ConversationDetailModal.tsx`
- Auto-polls `GET /api/dashboard/whatsapp-conversations/[id]` every 5 seconds
- Renders per-message `dir` attribute: `dir={message.language === "ar" ? "rtl" : "ltr"}`
- Staff reply → `POST .../reply` → flips `humanControlled: true`
- AI handoff toggle → `POST .../handoff`
- Right sidebar: booking summary, topics, conversation stats
- Arabic booking detection regex: `/(?:تم الحجز|تأكيد الموعد|تم تأكيد|موعد محجوز)/`

### `app/[lang]/dashboard/components/IntegrationStatusCards.tsx`
Top-of-dashboard status cards. WhatsApp card reads `integrationConfig.hasWhatsApp`:
- `true` → green "Connected / متصل"
- `false` → grey "Pending setup / في انتظار الإعداد"

`hasWhatsApp` is computed in `lib/services/dashboard.service.ts`:
```
hasWhatsApp = legacyN8n (user.n8nWebhookUrl) 
           || Twilio (org.twilioAccountSid + org.twilioWhatsappNumber)
           || Meta (org.whatsappPhoneNumberId)
```

### `components/dashboard/SuggestedFAQs.tsx`
Mounted inside KnowledgeBasePanel FAQs tab.
Surfaces AI-suggested FAQs derived from conversation learning.
- `GET /api/dashboard/suggested-faqs` — list suggestions
- One-click "Add to KB" — `POST /api/dashboard/suggested-faqs`

---

## Security

- **Org isolation**: every API endpoint checks `conversation.organizationId === caller's orgId`. 403 if mismatch.
- **Twilio signature validation**: every webhook verifies `x-twilio-signature` HMAC before processing.
- **Rate limiting**: Upstash Redis sliding window — 100 req/min per IP on the Twilio webhook.
- **Input sanitization**: `sanitizeChatMessage()` + `sanitizePhoneNumber()` on all inbound data.
- **Auth token encryption**: `twilioAuthTokenEncrypted` stored AES-encrypted; decrypted at runtime via `org-whatsapp-keys.service.ts`.
- **CSRF on Meta OAuth**: state token generated + verified per `lib/services/whatsapp-webhook-registration.ts`.
- **Human handoff auto-expiry**: `humanControlled` auto-resets after 30 min of inactivity (checked on each inbound message).
- **Staff message auth**: `POST .../reply` validates full `getAuthenticatedUser` session — no unauthenticated staff sends.

---

## Message Flow — Step by Step

```
1. Customer sends WhatsApp message
2. Twilio POSTs to /api/webhooks/twilio-whatsapp
3. Rate limit check (Upstash Redis)
4. Verify Twilio HMAC signature
5. whatsAppRouter resolves org from To number
6. Load org Twilio credentials
7. Parse & sanitize body + phone
8. detectLanguage(body) → "ar" | "en"

9. Upsert org_whatsapp_conversations (create if first message)
10. Fetch stored language preference (override detected lang if exists)

11. IF conversation.conversationState.phase === "selecting_language":
    → Parse 1/2/arabic/english choice
    → setLanguagePreference(orgId, phone, lang)
    → Send confirmation text
    → Return

12. IF isNewConversation && !storedLanguagePreference:
    → buildGreetingForOrg(orgId, orgName)
    → Send bilingual greeting
    → Set phase = "selecting_language"
    → Return

13. IF conversation.humanControlled && still active:
    → Store message only, skip AI
    → Return

14. Load last 20 messages for context
15. Store inbound message to org_whatsapp_messages
16. Build ConversationContext
17. getSectorConfigForIndustry(org.industry)
18. whatsAppAIEngine.processMessage(context, body, sectorConfig)
19. IF language === "ar": qatariDialectService.translateToQatari(responseText)
20. Store outbound message to org_whatsapp_messages
21. Update conversation (state, lastMessageAt, messageCount)
22. twilioWhatsAppService.sendTextMessage(credentials, from, responseText)
23. void analyzeConversationTurn(...) [non-blocking learning]
24. Return 200 empty (Twilio requirement)
```

---

## Environment Variables Required

```bash
# Twilio (per org — stored encrypted in DB, not env)
# Set via dashboard Setup > WhatsApp > Twilio Credentials
# org.twilioAccountSid, org.twilioAuthTokenEncrypted, org.twilioWhatsappNumber

# Meta Cloud API (per org — set via OAuth flow)
# org.whatsappPhoneNumberId, org.wabId

# Org-level encryption key
WHATSAPP_ENCRYPTION_KEY=<32-byte hex>

# Rate limiting
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# AI engine
ANTHROPIC_API_KEY=       # Qatari dialect (claude-haiku-4-5)
GOOGLE_API_KEY=          # Knowledge Base RAG (Gemini)
OPENROUTER_API_KEY=      # Main AI engine (via whatsapp-ai-engine.service.ts)

# Webhook registration (ngrok in dev)
NGROK_URL=https://xxxx.ngrok.io

# Meta App (for OAuth / Embedded Signup)
META_APP_ID=
META_APP_SECRET=
META_VERIFY_TOKEN=
```

---

## Known Duplicate Files (Safe to Delete)

These are macOS Finder copies created automatically. They are **not imported anywhere**:

```
mawidi-site/app/api/webhooks/twilio-whatsapp/route 2.ts
mawidi-site/app/api/dashboard/suggested-faqs/route 2.ts
mawidi-site/lib/config/sector-registry 2.ts
mawidi-site/lib/config/sector-default-faqs 2.ts
mawidi-site/lib/queue/post-booking-queue 2.ts
mawidi-site/lib/services/faq-suggestion.service 2.ts
mawidi-site/lib/services/meta-token-refresh.service 2.ts
mawidi-site/lib/services/conversation-learning.service 2.ts
mawidi-site/lib/services/conversation-cleanup.service 2.ts
mawidi-site/lib/services/whatsapp-webhook-registration 2.ts
mawidi-site/lib/services/org-provisioning.service 2.ts
mawidi-site/lib/services/whatsapp-router.service 2.ts
mawidi-site/lib/services/kb-seeder.service 2.ts
mawidi-site/components/dashboard/SuggestedFAQs 2.tsx
```

Delete with: `find mawidi-site -name "* 2.ts" -o -name "* 2.tsx" | xargs rm`

---

## Bug Fixes on This Branch

| Commit | Fix |
|---|---|
| `657d3495` | WhatsApp Inbox was querying old `whatsapp_conversations` table — fixed to `org_whatsapp_conversations` |
| `b312ee31` | `GET /[id]`, `POST /[id]/reply`, `POST /[id]/handoff` endpoints were missing — detail modal 5s poll was silently 404ing, leaving messages stuck at old timestamp |
| `86d0d1dd` | Real-estate viewings not saving from WhatsApp; calendar not loading them |
| `2c9ce575` | AI was hallucinating property listings — now grounded on real `viewings` Convex records |
| `a2ecd9d4` | Dashboard WhatsApp card stuck at "Pending setup" even when Twilio is connected — `integrationConfig` was hardcoded to `false` in `page.tsx` |

---

## What Still Needs To Be Done

- **Reply textarea RTL**: `ConversationDetailModal.tsx` textarea has no `dir` attribute — Arabic replies render LTR while typing. Add `dir="auto"` or `dir={conversation.language === "ar" ? "rtl" : "ltr"}`.
- **Arabic counter in dashboard stats**: `Arabic: 0` showing in the screenshot even with an active conversation — the stats endpoint may be querying `language === "ar"` on the conversation but conversations are stored as `"en"` until the user selects a language. Consider whether to count by last detected message language instead.
- **Cleanup duplicate files**: see table above.
- **Meta webhook verification**: test `GET /api/integrations/whatsapp/webhook` challenge response in production.
- **Post-booking queue**: `lib/queue/post-booking-queue.ts` exists but verify it fires after successful bookings from WhatsApp.
- **PR to master**: branch is 38 commits ahead. Needs rebase + PR when ready.
