# Direct Meta WhatsApp Cloud API — Operational Messaging Plan

## Status and scope

**Status:** Proposed implementation plan
**Owner:** CRM backend team
**Primary goal:** Add a direct Meta WhatsApp Business Platform (Cloud API) channel for:

1. config-driven, event-driven operational notifications to internal staff and external CRM contacts; and
2. explicit, manual WhatsApp delivery of quotations, sale orders, purchase orders, and onboarding documents.

This plan does **not** implement a campaign, bulk-marketing, chatbot, or inbox product. It does not alter
existing SMTP email or FCM delivery behaviour.

## Tenancy model: singleton config, no `account_id`

This deployment is effectively single-account. `AccountValidation` derives the account from `users.account_id`
and otherwise falls back to the first account; credentials and settings are stored as global singletons
(`SmtpCredential::first()`, `WabaCredential::query()->value(...)`, `Account::first()`,
`OnboardingSetting::current()`), and domain tables such as `quotations` have no `account_id` column.

Therefore, every new table in this plan is a **global singleton/record with no `account_id` column, no
per-account uniqueness, and no cross-account isolation logic or tests.** The connection, templates, and policies
are accessed as singletons (e.g. a `MetaWhatsAppConnection::current()` accessor) consistent with the rest of the
codebase. Introducing real multi-tenancy is explicitly out of scope; it would be a separate, cross-cutting
change far larger than this feature.

## Hard boundary: this is not the existing WABA integration

The application already contains a `waba_credentials` / `waba_templates` feature. It belongs to the
Bluewaves campaign/Journey integration and must remain separate:

| Existing feature | New feature in this plan |
|---|---|
| Bluewaves provider / campaign journeys | Direct Meta Graph API Cloud API |
| Global `waba_credentials` API key | WhatsApp columns added to the existing `meta_connections` record |
| Marketing/campaign templates and recipient batches | Operational event and document templates |
| Provider journey response/logging | Meta `wamid` and webhook delivery statuses |
| No direct Meta `/messages` request | Direct `/{PHONE_NUMBER_ID}/messages` requests |

Do not reuse the existing Bluewaves API key, `WabaCredential` model, Journey API client, campaign logs, or
campaign templates as the sender implementation. The existing `phone_number_id` and locally cached template
metadata may be used only as an administrator-verified migration reference; they are not trusted credentials
for the new direct integration.

## Reuse the existing `meta_connections` connection

WhatsApp Cloud API reuses the **same single `meta_connections` record** that today powers Meta Lead Ads, rather
than a separate connection table. One Meta app and one Meta connection back both features.

The current `meta_connections` table has: `meta_user_id`, `app_id`, `app_secret` (encrypted), `verify_token`,
`redirect_uri`, `graph_version`, `access_token` (encrypted), `token_expires_at`, `status`, `created_by`, plus a
`pages()` relation.

- **Shared (already present):** `app_id`, `app_secret` (encrypted), `graph_version`, `status`, `created_by`. The
  Meta *app* is app-level and legitimately common to Lead Ads and WhatsApp.
- **Added (new columns on `meta_connections`):** `waba_id`, `phone_number_id`, `whatsapp_access_token`
  (encrypted — a WABA-scoped system-user token), `whatsapp_verify_token` (encrypted — the WhatsApp webhook verify
  token, kept distinct from the existing Lead Ads `verify_token`), `whatsapp_display_phone_number` (nullable),
  `whatsapp_is_active`, `whatsapp_last_validated_at`, `whatsapp_last_validation_error` (nullable).

Constraints that still hold even though the record is shared:

1. **WhatsApp needs its own grant.** The existing Lead Ads user/page access tokens do not carry
   `whatsapp_business_messaging` / `whatsapp_business_management` and point at Pages, not a WABA. WhatsApp sends
   use the new `whatsapp_access_token` (a system-user token), never the Lead Ads `access_token` or a
   `page_access_token`.
2. **Independent enable/disable.** `whatsapp_is_active` governs WhatsApp independently of the Lead Ads `status`,
   so disabling one never silently disables the other.
3. **Separate webhooks.** The WhatsApp webhook (`/api/webhooks/meta/whatsapp`) validates against
   `whatsapp_verify_token` and the shared `app_secret`; it is distinct from the existing Lead Ads
   `/webhook/meta/lead`.
4. **Future Embedded Signup** (one unified Meta login that also grants WABA access and auto-populates
   `waba_id` / `phone_number_id` / `whatsapp_access_token`) is compatible with this shape but out of scope for
   v1; the columns are provided so admins can configure them manually first.

## Product rules

1. **FCM remains enabled.** WhatsApp is an additional policy-controlled channel, never an implicit replacement.
2. **No email coupling.** `send-email` endpoints never send WhatsApp. The new WhatsApp document endpoints never
   invoke SMTP.
3. **Manual documents only.** Creating, finalizing, or submitting a document/onboarding does not send WhatsApp.
4. **Template-first delivery.** Event and document sends outside a customer-service window use approved Meta
   utility templates. The CRM must not rely on arbitrary outbound text.
5. **Consent and opt-out are mandatory** for external contacts and for staff personal WhatsApp numbers.
6. A successful Meta send response means **accepted**, not delivered. Delivery/read/failure state is webhook-led.

## Meta prerequisites

Before code is enabled, the account administrator must provide or configure:

- Meta Business Portfolio and WhatsApp Business Account (WABA).
- A production WhatsApp business phone number and its `phone_number_id`.
- A Meta app connected to that WABA (the same Meta app already used for Lead Ads; its `app_id`/`app_secret` are
  reused from the shared `meta_connections` record).
- A dedicated system user token with the least WhatsApp permissions necessary for sending and media upload,
  stored as `whatsapp_access_token` on `meta_connections` — not the Lead Ads user/page token.
- A publicly reachable HTTPS webhook URL, webhook verify token, and the Meta app secret used for signature checks.
- Approved, active utility templates in each required language.
- Documented recipient consent and an opt-out workflow.

Required initial templates:

| Template key | Purpose | Header |
|---|---|---|
| `meeting_scheduled` | Meeting scheduled/rescheduled | Text |
| `sale_approved` | Approved sale notice | Text |
| `payment_reminder` | Payment reminder | Text |
| `quotation_document` | Quotation PDF | Document |
| `sale_order_document` | Sale order PDF | Document |
| `purchase_order_document` | Purchase order PDF | Document |
| `onboarding_document` | Onboarding summary/PDF | Document |

Template names, language, and parameter/component layout vary by configuration and must be configured rather than
hardcoded.

## Existing touchpoints (verified against the codebase)

The first release covers only the following existing notification paths:

| Business trigger | Existing source | Existing delivery | New WhatsApp policy key |
|---|---|---|---|
| Meeting scheduled | `MeetingController` (create/schedule) | FCM | `meeting.scheduled` |
| Meeting rescheduled | `MeetingController::meeting_reschedule` (distinct method) | FCM | `meeting.rescheduled` |
| Sale approval state changed | `FinanceController::approve_sale` | FCM to sale creator (`notifyUser($sale->creator, ...)`) | `sale.approval_changed` |
| Payment reminder due | `SendPaymentRemindersJob` | FCM to resolved staff recipients | `payment.reminder_due` |

Existing document email endpoints that need parallel, manual WhatsApp endpoints (all three email triplets
`send-email` / `email-history` / `resend-email` confirmed present in `routes/app.php`):

| Document | Current email route | New WhatsApp route |
|---|---|---|
| Quotation | `POST /api/quotation/send-email` | `POST /api/quotation/send-whatsapp` |
| Purchase order | `POST /api/purchase-order/send-email` | `POST /api/purchase-order/send-whatsapp` |
| Sale order | `POST /api/sale-order/send-email` | `POST /api/sale-order/send-whatsapp` |
| Onboarding | Email is currently automatic after submit (no manual route) | `POST /api/onboarding/send-whatsapp` |

Onboarding has no manual email-send route today. Its new WhatsApp endpoint is intentionally explicit and must
not be called by `OnboardingController::create`.

## Permission names (reconciled against the real `PermissionCatalog`)

Use the existing module hierarchy. Settings-area permissions live under the existing `setting` module (which
already houses `waba_credential`, `meta_credential`, etc.); document-send permissions extend the existing
document sub-modules:

```text
setting.meta_whatsapp_connection.{read,update,override}
setting.meta_whatsapp_policy.{read,update,override}
setting.meta_whatsapp_log.{read,override}
quotation_management.quotation.send_whatsapp
purchase_order_management.purchase_order.send_whatsapp
sale_order_management.sale_order.send_whatsapp
onboarding_management.onboarding.send_whatsapp
```

Add these to `PermissionCatalog`, `PermissionSeeder`, and tests. Do not weaken current read/update/override
checks merely to add sending.

Because the WhatsApp connection now lives on the shared `meta_connections` record (which `MetaConnectionController`
already guards with `setting.meta_credential.*`), the WhatsApp connection endpoints may either introduce the new
`setting.meta_whatsapp_connection.*` permission (recommended — lets an admin manage WhatsApp without also granting
Lead Ads connection edits) or reuse `setting.meta_credential.*`. This plan assumes the dedicated
`setting.meta_whatsapp_connection.*` permission; the policy/log/document permissions above are unaffected either
way.

## Target architecture

```text
Domain transaction / explicit document endpoint
                  |
                  v
      typed notification intent + transactional outbox
                  |
                  v
          queued notification dispatcher
             |                    |
             v                    v
      existing FCM adapter   Direct Meta WhatsApp adapter
                                      |
                                      v
                           Meta Graph API + media upload
                                      |
                                      v
                            WhatsApp webhook receiver
                                      |
                                      v
                        message/delivery audit records
```

The dispatcher owns recipient resolution, policy evaluation, opt-in checks, and idempotency. Controllers and
scheduled jobs publish typed intents only; they do not make network calls to Meta.

> **v1 simplification option:** the transactional-outbox + dispatcher abstraction is the highest-risk part
> because it re-routes working FCM calls. A leaner v1 can skip the outbox and dispatch `SendMetaWhatsAppMessageJob`
> directly at each touchpoint (leaving existing FCM calls untouched), adding the outbox later. This plan documents
> the full outbox design; note the trim option per touchpoint if reduced blast radius is preferred.

## Data model

Create the following migrations, models, relationships, factories, resources, and indexes. **No `account_id`
columns.**

### 1. `meta_connections` extension (no new connection table)

Do **not** create a separate connection table. Add WhatsApp columns to the existing `meta_connections` table via
a new migration:

- `waba_id`, `phone_number_id`
- `whatsapp_access_token` (encrypted cast — WABA-scoped system-user token)
- `whatsapp_verify_token` (encrypted cast — WhatsApp webhook verify token, distinct from Lead Ads `verify_token`)
- `whatsapp_display_phone_number` (nullable), `whatsapp_is_active` (default false)
- `whatsapp_last_validated_at`, `whatsapp_last_validation_error` (nullable)

Model changes on `MetaConnection`: add the columns to `$fillable`; add `whatsapp_access_token` and
`whatsapp_verify_token` to the `encrypted` casts and `whatsapp_last_validated_at` to the datetime casts. Add a
`MetaConnection::current()` accessor (mirrors `SmtpCredential::first()` / `OnboardingSetting::current()`) so the
WhatsApp code reads the single shared record. Return only masked values from Resources; never expose the tokens.

The `connection_id` FKs on `meta_whatsapp_templates` and `meta_whatsapp_webhook_events` reference
`meta_connections`.

### 2. `meta_whatsapp_templates`

A global cache/mapping of approved templates.

- `id`, `connection_id` (nullable FK)
- `key` (application key), `meta_template_name`, `language`, `category`, `status`
- `components` (JSON schema snapshot), `variable_map` (JSON), `is_active`, timestamps
- unique index: `key + language`

`components` allows the service to validate header/body/button parameter counts before queueing a send.

### 3. `meta_whatsapp_notification_policies`

Config-driven event behaviour, not executable code stored in JSON.

- `id`, `event_key` (unique), `template_id`, `language`
- `channels` JSON (initially `fcm`, `whatsapp`), `recipient_rules` JSON, `conditions` JSON
- `is_active`, `created_by`, timestamps

Recipient rules are a strict enum interpreted by a service (for example `sale_creator`, `meeting_assignee`,
`meeting_companions`, `lead_primary_contact`, `billing_contact`), never arbitrary class/method names.

### 4. `meta_whatsapp_contact_preferences`

- `id`, `contact_type`, `contact_id` (nullable)
- `phone_e164`, `phone_hash`, `opted_in_at`, `opt_in_source`, `opted_out_at` (nullable)
- `created_by`, timestamps
- indexes: `phone_hash`, `contact_type + contact_id`

`phone_e164` must be encrypted or access-controlled according to the CRM's privacy policy; `phone_hash` supports
safe lookups and deduplication.

### 5. `notification_outbox`

A provider-neutral durable intent record, written in the same transaction as the domain change.

- `id`, `event_key`, `notifiable_type`, `notifiable_id`
- `payload` JSON, `idempotency_key`, `status`, `available_at`, `processed_at`, `last_error`, timestamps
- unique index on `idempotency_key`; index on `status + available_at`.

### 6. `meta_whatsapp_message_batches` and `meta_whatsapp_messages`

`batches` groups a manual document action. `messages` is one row for every recipient/message.

- Batch: `document_type`, `document_id`, `requested_by`, `status`, timestamps.
- Message: `batch_id` nullable, recipient reference, encrypted/canonical phone, template snapshot,
  Meta `wamid`, `status`, response/error JSON, `accepted_at`, `delivered_at`, `read_at`, `failed_at`, timestamps.
- Enforce an idempotency key per outbound recipient/template/entity revision.

### 7. `meta_whatsapp_webhook_events`

- `id`, `connection_id` nullable, `event_key`, `meta_event_id` nullable, `payload` JSON
- `signature_valid`, `status`, `processed_at`, `error_message`, timestamps
- unique index on a deterministic event fingerprint/deduplication key.

## Services and jobs

### `PhoneNumberNormalizer`

A reusable service for user, lead, and external recipient numbers. It combines country code and local phone into
digits-only E.164-compatible form (India `91` default), validates it, and never mutates legacy `leads.phone`
values.

### `MetaWhatsAppCloudApiClient`

Use Laravel's HTTP client with explicit connect/read timeouts and no secret logging.

- `validateConnection(MetaConnection $connection): ConnectionResult`
- `uploadMedia(...): MediaUploadResult`
- `sendTemplate(...): SendResult`
- `sendSessionText(...)` (not used by phase-one events unless a future inbound conversation scope permits it)

The client reads the shared `meta_connections` record via `MetaConnection::current()`, uses its
`whatsapp_access_token`, posts only to the configured Meta Graph API base/version, and calls the connection's
`/{phone_number_id}/messages` endpoint for sends. It must classify transient failures for retry and persist safe,
redacted error details.

### `WhatsAppTemplateRenderer`

Resolves only configured variables, validates components against the cached schema, and creates Meta payloads.
It must reject missing or extra parameters before an API call.

### `WhatsAppRecipientResolver`

Resolves recipients from policy enums, deduplicates numbers, checks opt-in/out state, and produces an explicit
skip reason for ineligible recipients.

### `NotificationDispatcher` and jobs

- `DispatchNotificationOutboxJob` reads due outbox rows and creates channel-specific work.
- `SendMetaWhatsAppMessageJob` sends exactly one recipient/message and uses bounded retry/backoff for network,
  rate-limit, and 5xx failures.
- `ProcessMetaWhatsAppWebhookJob` processes persisted webhook events asynchronously and idempotently.
- Use dedicated `notifications` and `meta-whatsapp-webhooks` database queues; document worker configuration.

## Domain-event integration

Create typed events/intents such as `MeetingScheduled`, `MeetingRescheduled`, `SaleApprovalChanged`, and
`PaymentReminderDue`. They must be emitted only after the surrounding database transaction commits.

Implementation changes:

1. Route the stated events through the dispatcher intent while preserving FCM as a configured delivery adapter
   (existing FCM behaviour must be unchanged when WhatsApp is disabled).
2. In `FinanceController::approve_sale`, move notification dispatch out of the open transaction. The current FCM
   notification happens inside the flow; the new outbox record must be created atomically and processed after
   commit.
3. In `SendPaymentRemindersJob`, preserve its daily `ScheduledNotificationLog` deduplication. The WhatsApp
   idempotency key includes the existing reminder log/event identity so retries never cause duplicate sends.
4. `meeting.scheduled` and `meeting.rescheduled` map to the meeting create/schedule path and the distinct
   `meeting_reschedule` method respectively.
5. Do not expand other existing FCM notifications in phase one. New events require an explicit policy/template and
   tests before they are enabled.

## Explicit document delivery

Each new endpoint uses a Form Request, authorization policy, service, job, API Resource, and activity record.

### Request contract

Example for a quotation:

```json
{
  "quotation_id": 42,
  "recipient_phone_numbers": ["919876543210"],
  "template_key": "quotation_document",
  "language": "en_US",
  "variables": {
    "recipient_name": "Asha Shah",
    "document_number": "QTN-00042"
  }
}
```

The equivalent identifier is `purchase_order_id`, `sale_order_id`, or `onboarding_id`. Phone numbers are accepted
only after permission checks and normalization. The API must not accept an arbitrary Meta template name — only a
configured `template_key`.

### Delivery flow

1. Authorize the relevant `*.send_whatsapp` permission (or `.override`).
2. Confirm the document is visible to the caller and in a sendable state.
3. Resolve the configured approved utility template.
4. Generate the existing document PDF with its existing PDF service.
5. Upload the PDF to Meta media storage, then send the document-header template using the media ID.
6. Create a delivery batch and recipient message rows, then queue jobs.
7. Return `202 Accepted` with batch and initial queued state. Do not claim delivery until the webhook records it.

Version one sends the generated primary PDF only. Existing email-only optional attachments are deliberately out of
scope: each separate WhatsApp document needs its own approved document-template/send strategy, particularly
outside the customer-service window.

Add read-only histories:

```text
POST /api/quotation/whatsapp-history
POST /api/purchase-order/whatsapp-history
POST /api/sale-order/whatsapp-history
POST /api/onboarding/whatsapp-history
```

## Webhook design

Add public routes outside Sanctum/account validation:

```text
GET  /api/webhooks/meta/whatsapp
POST /api/webhooks/meta/whatsapp
```

- `GET`: validate Meta's challenge and the stored verification token.
- `POST`: validate `X-Hub-Signature-256` against the app secret before persistence.
- Store the raw event, return promptly, then enqueue processing.
- Deduplicate retries/replays before status transitions.
- Transition a message through `accepted`, `sent`, `delivered`, `read`, or `failed` as Meta reports it.
- Phase one records inbound messages only for audit/opt-out processing. It does not create an agent inbox or
  autonomous replies.

## Management APIs and permissions

Add a `meta_whatsapp` settings area with Form Requests and Resources for:

- connection fetch/update/test/disable — operating on the WhatsApp fields of the shared `meta_connections`
  record (masked), not a separate connection resource;
- template fetch/sync/activate/deactivate;
- event-policy fetch/update;
- recipient opt-in/opt-out and preference history;
- delivery history and controlled retry of eligible failures.

Register `POST` routes under a `meta_whatsapp` prefix with `permission:` middleware, using the permission names in
the section above.

## Security and reliability requirements

- Use encrypted casts for all direct-Meta secrets; never expose them from a Resource, controller response,
  exception, activity log, or queue payload.
- Mask recipient numbers in ordinary API responses and logs.
- Validate the Meta webhook signature before persisting or processing payloads.
- Use queue retry/backoff and idempotency keys; do not retry permanent template, consent, or invalid-number errors.
- Rate-limit manual send endpoints; log requester, target document, recipient count, template key, and outcome.
- Record consent source/time and support opt-out as an immediate hard stop.
- Define retention and access rules for message bodies, phone numbers, webhook payloads, and delivery metadata.

## Test plan

### Unit tests

- phone normalization and invalid-number handling;
- template component/variable validation;
- recipient rules, deduplication, and consent (no cross-account isolation — single-config model);
- error classification, redaction, idempotency keys, and retry rules;
- webhook HMAC verification and payload status parsing.

### Feature tests

- connection and policy authorization/masked secret responses;
- outbox rows are created only after a successful source transaction;
- existing FCM notifications remain queued/sent when WhatsApp is disabled;
- each event sends only when its policy, approved template, valid recipient, and opt-in all exist;
- all four manual document endpoints create a WhatsApp batch without sending email;
- existing `send-email` endpoints never create WhatsApp batches;
- PDF/media-upload/template-send payloads use the expected Meta client contract;
- duplicate job execution and webhook replay do not duplicate recipient delivery records;
- delivered/read/failed webhook statuses update the correct `wamid` row.

### UAT / production checks

1. Connect a Meta test number and submit one approved template to approved test recipients.
2. Verify document-header media delivery for all four document types.
3. Verify 24-hour window/template behaviour with Meta's test tooling.
4. Verify no staff/customer message is sent with a disabled policy or withdrawn consent.
5. Verify webhook challenge, invalid-signature rejection, accepted, delivered, read, and failed status handling.
6. Enable production initially with internal recipients only; monitor failure/error rate before enabling external
   recipient policies.

## Delivery phases

1. **Foundation:** migration extending `meta_connections` with WhatsApp columns (encrypted tokens) + a
   `MetaConnection::current()` accessor, Meta client, masked resources, connection test, webhook verification,
   audit models.
2. **Configuration:** templates, consent/preferences, policies, permissions, and settings APIs.
3. **Dispatcher:** transactional outbox, recipient resolver, FCM adapter preservation, jobs, observability.
   (Optionally trimmed in v1 to direct job dispatch per touchpoint — see architecture note.)
4. **Domain events:** meeting scheduled/rescheduled, sale approval, and payment reminder policies, starting
   disabled by default.
5. **Documents:** the four explicit send-via-WhatsApp endpoints, primary-PDF media delivery, history APIs.
6. **Rollout:** test WABA, internal pilot, webhook monitoring, external-contact enablement, support runbook.

## Documentation updates required during implementation

- `docs/api.md`: new routes, request/response contracts, webhook notes, and `202 Accepted` semantics.
- `docs/database.md`: the new WhatsApp columns on `meta_connections`, all new tables, foreign keys, encryption,
  indexes, and retention notes.
- `docs/architecture.md`: dispatcher/outbox/job/webhook topology and queue worker requirements.
- `docs/business-rules.md`: event recipient rules, consent, opt-out, and template-first policy.
- `docs/deployment.md`: Meta environment values, public webhook URL, queue workers, and secret rotation.
