# WHATSAPP_FLOWS — WhatsApp Flows (Meta oficial e Gupshup)

| Campo | Valor |
| :---- | :---- |
| Status | Implementado (envio, CRUD, decodificação de resposta inbound, Data Exchange criptografado) + front-end completo |
| Escopo de parceiro | **Meta Cloud API** e **Gupshup** (ver [Padrão: orquestrador multi-parceiro](../architecture/ARCHITECTURE.md#padrão-orquestrador-multi-parceiro-coreomnimetausecase)). Evolution API ainda não suportado |
| ADR | Nenhum dedicado — decisões registradas nesta doc |

## Escopo

| Incluído | Excluído / futuro |
| :------- | :----------------- |
| Enviar mensagem `interactive.flow` (Fase 1) — Meta e Gupshup | Flow em Evolution API (orquestrador já pronto, falta implementação por parceiro) |
| CRUD do Flow — create/update/publish/deprecate/delete/preview (Fase 2) — Meta (Graph API direta) e Gupshup (Partner Flow API) | Editor visual com galeria de templates prontos da Meta |
| Decodificar `nfm_reply` do webhook inbound (Fase 3) | Lógica de negócio por tela no Data Exchange (endpoint existe, handler é `Null`) |
| Data Exchange criptografado — endpoint + RSA/AES (Fase 4) — **Meta apenas** | Data Exchange na Gupshup (BSP não suporta — só "Flow sem endpoint") |
| Front-end: listar/criar/editar/depreciar/deletar, editor visual de telas, preview local, teste de envio (partner-agnóstico) | Reconciliação de erros assíncronos de Flow (`FLOWS_STATUS_UPDATE` da Meta) |

**Público-alvo:** desenvolvedores OmniMeta, QA, suporte técnico.

## Objetivo

1. Permitir que o cliente do OmniMeta envie formulários nativos do WhatsApp (WhatsApp Flows) sem sair do chat.
2. Gerenciar o ciclo de vida do Flow (rascunho → publicado → depreciado) via API OmniMeta, sem o cliente precisar chamar a Graph API da Meta diretamente.
3. Trazer a resposta do formulário de volta pro fluxo normal de mensagens/webhook do OmniMeta.
4. Suportar telas dinâmicas (que buscam dado no backend) via o protocolo de Data Exchange criptografado da Meta.

## Conceitos — Meta

- **Flow**: recurso da **WABA** (não do número de telefone). Qualquer número daquela WABA pode enviar qualquer Flow dela.
- **Ciclo de vida**: `DRAFT` (editável, deletável) → `PUBLISHED` (imutável, irreversível) → `DEPRECATED` (não editável nem enviável, mas **permanece no histórico** — nunca é hard-delete) / `BLOCKED` / `THROTTLED` (definidos pela Meta).
- **`flowJson`**: schema de telas (`screens[]`) e componentes (`layout.children[]`). Versão mínima usada: `"7.0"`.
- **Chave de criptografia (Business Encryption)**: `POST /{phone-number-id}/whatsapp_business_encryption` — é **por número de telefone**, não por Flow nem por WABA.

## Estado atual (evidência no código)

| Tema | Situação | Arquivo / evidência |
| :--- | :------- | :------------------ |
| Enum de conteúdo de mensagem | `MessageContentsEnum::FLOW = 'flow'` | `api/src/Core/Domain/Enum/Meta/Messages/MessageContentsEnum.php` |
| Envio (Fase 1) | Resolve `flowId` **local** do OmniMeta → `flow_id` real da Meta; valida `PUBLISHED` | `api/src/Core/UseCase/Message/ProcessFlowUseCase.php` |
| Validação de campos de envio | `header`/`footer`/`body`/`flowCta`/`flowActionPayload` | `api/src/Core/Domain/Validation/Message/FlowValidation.php` |
| Entity / enums | `Flow`, `FlowStatusEnum`, `FlowCategoriesEnum` | `api/src/Core/Domain/Entity/Flow.php`, `api/src/Core/Domain/Enum/Meta/Flow/` |
| Persistência | Collection `flows` (Mongo) | `api/app/Models/Flow.php`, `api/database/migrations/2026_08_26_120000_create_flows_table.php` |
| Cliente Graph API (Meta) | Create/update/publish/deprecate/delete/preview/business-encryption | `api/app/Services/Meta/WhatsAppFlowsApiService.php` |
| Cliente Partner Flow API (Gupshup) | Create/update/publish/deprecate/delete/preview — mesmo shape de request/response do Meta | `api/app/Services/Gupshup/GupshupFlowsApiService.php` |
| Implementação Gupshup do CRUD | 6 use cases (Create/Update/Publish/Deprecate/Delete/Preview), resolvem `appId`/`appSecret` via `GraphApplication` (não usam `wabaId`/`accessToken` da `WhatsappAccount`, que pra Gupshup são valores sintéticos) | `api/src/Core/Gupshup/UseCase/Flow/*` |
| Validação local de `flowJson` (Fase 2) | `screens[].id` só `[A-Za-z_]+`; labels/texts obrigatórios não-vazios — roda **antes** de chamar Meta/Gupshup (regra é da Meta, vale pros dois BSPs) | `api/src/Core/Domain/Validation/Flow/FlowJsonValidation.php` |
| Erros reais de publicação | Busca `validation_errors` (Graph API ou Gupshup, formatos diferentes normalizados pro mesmo shape) quando o publish falha e devolve `path`/`error`/`message` estruturado | `api/src/Core/UseCase/Flow/PublishFlowUseCase.php` (Meta), `api/src/Core/Gupshup/UseCase/Flow/PublishFlowUseCase.php` (Gupshup), `FlowPublishValidationException` |
| Orquestrador multi-parceiro (CRUD) | Recebe `Partner` + `WhatsappAccount` já resolvidos; delega pra Meta ou Gupshup, ou lança erro claro pra Evolution API | `api/src/Core/OmniMeta/UseCase/Flow/*` — ver seção [Orquestrador](#orquestrador-multi-parceiro) |
| Envio via Gupshup | `flow` adicionado ao strategy map por tipo de conteúdo — reaproveita o `ProcessFlowUseCase` do Meta sem alteração (payload `interactive.flow` é idêntico nos dois BSPs) | `api/src/Core/Gupshup/UseCase/GupshupSendWhatsappMessageUseCase.php` |
| API REST | 9 endpoints (`FlowController`) | `api/app/Http/Controllers/FlowController.php`, `api/routes/api.php` |
| Decodificação inbound (Fase 3) | `nfm_reply` vira `response` decodificado antes de persistir/disparar webhook do cliente | `api/src/Core/Application/Webhook/Messages/MessageProcessing.php`, `api/src/Core/UseCase/Webhook/MessageWebhookTriggerUseCase.php` |
| Data Exchange (Fase 4) | Endpoint dedicado + criptografia RSA-OAEP-SHA256 (chave AES) + AES-128-GCM (corpo) + IV invertido na resposta | `api/app/Http/Controllers/FlowDataExchangeController.php`, `api/app/Services/Meta/Flow/FlowEncryptionService.php`, `api/src/Core/UseCase/Flow/ProcessFlowDataExchangeUseCase.php` |
| Handler de negócio do Data Exchange | **Não implementado** — qualquer Flow que dependa disso recebe erro 500 de propósito | `api/app/Services/Meta/Flow/NullFlowDataExchangeHandler.php` |
| Chave de criptografia por telefone | RSA 2048 gerado, pública enviada à Meta, privada guardada criptografada (`Crypt::encryptString`) | `api/src/Core/UseCase/Flow/GenerateFlowEncryptionKeyUseCase.php`, campos `flow_public_key`/`flow_private_key`/`flow_public_key_status` em `phones` |
| Front-end — menu | "Flows" ao lado de "Templates" (criar / listar) | `front/src/app/configs/navigationConfig.ts` |
| Front-end — listagem | Tabela com editar/publicar/depreciar/deletar/visualizar/testar envio (status-gated) | `front/src/app/main/apps/flow/list/FlowListTable.tsx` |
| Front-end — editor visual | Telas + componentes + preview em formato de celular, alternativa ao JSON cru | `front/src/app/main/apps/flow/builder/FlowVisualBuilder.tsx` |
| Front-end — preview | Modal **local** (renderiza o `flow_json` salvo) — não abre link externo da Meta | `front/src/app/main/apps/flow/FlowPreviewDialog.tsx` |
| Front-end — erros de publish | Mostra os `validation_errors` reais da Meta em Dialog | `front/src/app/main/apps/flow/FlowValidationErrorsDialog.tsx` |
| Bruno (requests de teste) | Pasta `Flow` (9 requests) + `Message/Send Flow` + `Webhook/Facebook partner - Flow Reply Received` | `request-collections/OmniMeta/Bruno/Flow/`, `.../Message/Send Flow.bru` |

## Arquitetura

```mermaid
flowchart TD
  subgraph Envio
    Cliente1[Cliente OmniMeta] -->|POST channels/whatsapp/messages type=flow, flowId=local| SendMsg[SendWhatsappMessageUseCase]
    SendMsg --> ProcessFlow[ProcessFlowUseCase resolve flowId local to flowId Meta]
    ProcessFlow -->|valida PUBLISHED| CloudApi[Graph API messages]
  end

  subgraph CRUD
    Cliente2[Cliente OmniMeta] -->|POST/PUT/.../flows| FlowCtrl[FlowController]
    FlowCtrl --> Orq[Core.OmniMeta.UseCase.Flow.X]
    Orq -->|partner Meta| MetaUC[Core.UseCase.Flow.X]
    Orq -->|partner Gupshup| GupUC[Core.Gupshup.UseCase.Flow.X]
    Orq -->|Evolution API| Err[throw FlowException]
    MetaUC --> GraphFlows[Graph API flows]
    GupUC --> GupFlows[Gupshup Partner Flow API]
    MetaUC --> MongoFlows[(flows)]
    GupUC --> MongoFlows
  end

  subgraph Inbound
    Meta[Meta] -->|webhook nfm_reply| WebhookCtrl[WebhookController]
    WebhookCtrl --> MsgProc[MessageProcessing decode nfm_reply]
    MsgProc --> MongoMsg[(messages)]
    MsgProc --> Trigger[MessageWebhookTriggerUseCase]
    Trigger --> ClienteWH[Webhook outbound do cliente]
  end

  subgraph DataExchange
    Meta -->|POST encrypted| DXCtrl[FlowDataExchangeController]
    DXCtrl --> DXUseCase[ProcessFlowDataExchangeUseCase]
    DXUseCase -->|decrypt RSA+AES| Handler[FlowDataExchangeHandlerInterface = Null hoje]
    DXUseCase -->|encrypt AES resposta| Meta
  end
```

### Orquestrador multi-parceiro

O CRUD de Flow segue o padrão descrito em [ARCHITECTURE.md](../architecture/ARCHITECTURE.md#padrão-orquestrador-multi-parceiro-coreomnimetausecase):

- Middlewares (`FlowMiddleware`, `FlowResourceMiddleware`, `FlowEncryptionKeyMiddleware`) resolvem `WhatsappAccount` + `Partner` e anexam na request — **sem bloquear** por partner.
- `FlowController` chama só `Core\OmniMeta\UseCase\Flow\*` (Create/Update/Publish/Deprecate/Delete/Preview/GenerateEncryptionKey).
- Cada orquestrador valida o partner (`PartnersEnum::tryFrom`) e delega: `Meta` → `Core\UseCase\Flow\*`, `Gupshup` → `Core\Gupshup\UseCase\Flow\*`. Qualquer outro partner (hoje: Evolution API) lança `FlowException` ("WhatsApp Flows are not yet supported for the X partner") em vez de chamar uma API com credenciais erradas. `GenerateFlowEncryptionKeyUseCase` rejeita Gupshup explicitamente (Data Exchange não é uma capability da Gupshup, não só "ainda não implementado").
- As duas implementações compartilham os mesmos DTOs (`Core\UseCase\Flow\DTO\*`) — o orquestrador monta um DTO só e passa pra Meta ou Gupshup sem transformação.
- `ListFlowsUseCase` e `ShowFlowUseCase` **não** passam pelo orquestrador — só leem a collection local `flows`, são partner-agnósticos.
- Envio de Flow (Fase 1) já usava o padrão desde o início: `ProcessFlowUseCase` é só mais uma entrada no strategy map por tipo de conteúdo do `SendWhatsappMessageUseCase`/`GupshupSendWhatsappMessageUseCase` (igual `template`/`text`) — o mesmo `ProcessFlowUseCase` do Meta é reaproveitado sem alteração pela Gupshup, já que o payload `interactive.flow` é byte-a-byte idêntico nos dois BSPs.
- Diferenças reais entre os dois BSPs: Gupshup não suporta Data Exchange (só "Flow sem endpoint"), o `DELETE` da Gupshup usa `/flow/{id}` (singular, não `/flows/{id}`), e `validation_errors` da Gupshup são strings simples em vez do objeto `{pointers, error, message}` da Meta — normalizado pro mesmo shape `{path, error, message}` antes de virar `FlowPublishValidationException`.

Pra adicionar Gupshup Flow no futuro: criar `Core\Gupshup\UseCase\Flow\*` e trocar o `throw` por `$this->gupshupUseCase->execute(...)` em cada orquestrador — nenhuma rota, controller ou DTO externo muda.

## Contratos

### Collection `flows` (Mongo)

```json
{
  "id": "uuid",
  "whatsapp_account_id": "uuid",
  "flow_id": "1087332970516666",
  "name": "cadastro_lead",
  "categories": ["LEAD_GENERATION"],
  "status": "PUBLISHED",
  "flow_json": { "version": "7.0", "screens": [] },
  "preview_url": "https://business.facebook.com/wa/manage/flows/.../preview/?token=...",
  "created_at": "...",
  "updated_at": "..."
}
```

`flow_id` é o id **da Meta**; `id` é o id **local do OmniMeta**, usado em toda a API do OmniMeta (inclusive no envio — ver abaixo).

### Campos de criptografia em `phones`

`flow_public_key`, `flow_private_key` (criptografado com `APP_KEY`), `flow_public_key_status`.

### API REST (`/api/v1/flows*`)

| Método / rota | Uso |
| :------------ | :-- |
| `GET /flows` | Lista (filtros `whatsappAccountId`, `phoneNumber`, `status`) — `phoneNumber` é o número real, resolvido pra `whatsappAccountId` dentro de `ListFlowsUseCase` (nunca no controller) |
| `GET /flows/{id}` | Detalhe |
| `POST /flows` | Cria (`DRAFT`); aceita `whatsappAccountId` OU `phoneNumber` (mesma convenção do envio de mensagem — `FlowMiddleware` resolve qualquer um dos dois); `flowJson` opcional já na criação |
| `PUT /flows/{id}` | Edita — só em `DRAFT` |
| `POST /flows/{id}/publish` | `DRAFT → PUBLISHED`, irreversível |
| `POST /flows/{id}/deprecate` | `PUBLISHED → DEPRECATED` — não some do banco |
| `DELETE /flows/{id}` | Só `DRAFT` |
| `GET /flows/{id}/preview` | Link de preview da Meta (expira em 30 dias) |
| `POST /flows/phones/{phoneId}/encryption-key` | (Re)gera a chave RSA de Data Exchange pro número |

Erros de partner não suportado: 500 com `message` explicando qual partner. Erros de `flowJson` inválido (regra local): 500 com `message` apontando o `screens[]`/`children[]` exato. Erros de publish rejeitado pela Meta: 500 com `message` + `validation_errors[]` (`path`, `error`, `message` reais da Graph API).

### Envio (`POST /api/v1/channels/whatsapp/messages`, `contents[].type = "flow"`)

```json
{
  "from": "5511999999999",
  "to": "5511988888888",
  "contents": [{
    "type": "flow",
    "body": "Vamos começar seu atendimento",
    "footer": "OmniMeta",
    "flowId": "72ba8a0c-0f11-4c89-af82-24df0bd95e4b",
    "flowCta": "Iniciar"
  }]
}
```

`flowId` é o **id local do OmniMeta** (igual `templateId` em templates) — o backend resolve pro `flow_id` real do parceiro e valida que o Flow está `PUBLISHED`. `flowActionPayload` é opcional: quando omitido, `ProcessFlowUseCase` usa automaticamente `flow_json.screens[0].id` (a tela de entrada) — mesma lógica que o botão "Testar envio" do front já aplicava manualmente. Só é necessário informar `flowActionPayload.screen` pra sobrescrever esse padrão, e nesse caso precisa ser especificamente a tela de entrada — uma tela real porém não-inicial também falha com `(#131009) Parameter value is not valid` (a Meta não permite abrir a mensagem direto no meio do fluxo).

### Webhook inbound — resposta do formulário

A Meta manda `interactive.type = "nfm_reply"` com `response_json` como **string JSON dentro do JSON**. O OmniMeta decodifica isso antes de persistir e antes de repassar pro webhook outbound do cliente — o cliente recebe um objeto de verdade, nunca uma string escapada.

## Regras aprendidas na prática (não documentadas claramente pela Meta)

- **Versão do `flowJson`**: usar `"7.0"` — versões antigas (`"5.0"`) são rejeitadas no publish.
- **`screens[].id`**: só letras e underscore, **nenhum dígito** (`"WELCOME_1"` inválido, `"WELCOME"` válido) — validado localmente antes da Graph API.
- **Label vazio** em campo de input (TextInput/TextArea/RadioButtonsGroup/CheckboxGroup/Dropdown): rejeitado pela Meta — validado localmente.
- **Interpolação `${data.x}`**: não funciona misturada com texto estático na mesma string — cada propriedade de texto tem que ser 100% estática ou 100% um único `${...}`.
- **Objeto vazio `{}`**: `json_decode(..., true)` em PHP colapsa `{}` e `[]` na mesma representação; o controller decodifica com `json_decode(..., false)` (modo `stdClass`) pra preservar a distinção ao repassar pra Meta.
- **Tela terminal obrigatória**: pelo menos uma tela com `terminal: true` — senão o publish falha com `MISSING_TERMINAL_SCREEN`.
- **Todas as telas conectadas**: toda tela precisa ser alcançável por uma ação `navigate` — senão falha com `INVALID_ROUTING_MODEL`.
- **`flowActionPayload.screen` só aceita a tela de entrada**: uma mensagem de Flow não pode abrir a experiência já numa tela do meio, mesmo que o id seja de uma tela real do Flow — falha com `(#131009) Parameter value is not valid`. É por isso que `ProcessFlowUseCase` usa sempre `screens[0]` como padrão quando o campo é omitido.

## Limites e dependências

- Meta oficial e Gupshup têm Flow implementado (ver seção Orquestrador); Evolution API ainda não.
- Exige a feature de Flows habilitada na WABA no Business Manager da Meta (vale pros dois BSPs).
- Data Exchange (telas dinâmicas que chamam o backend) só existe pra Meta — a Gupshup só suporta "Flow sem endpoint" (limite documentado pelo próprio parceiro, não uma lacuna de implementação). Mesmo na Meta, a criptografia está pronta mas **sem lógica de negócio por tela** — qualquer Flow que dependa disso quebra de propósito até um handler real ser implementado.
- Chave de criptografia de Data Exchange precisa ser gerada uma vez por número de telefone antes de enviar um Flow Meta que use telas dinâmicas.

## Critérios de aceite

- [x] Enviar Flow publicado usando id local (resolvido pro `flow_id` real da Meta/Gupshup)
- [x] CRUD completo (create/update/publish/deprecate/delete/preview) via API OmniMeta — Meta e Gupshup
- [x] `flowJson` validado localmente antes de chamar a Graph API/Gupshup (screen id, labels)
- [x] Erros reais de publish expostos (backend + front) — Meta e Gupshup
- [x] Depreciar não apaga o Flow do banco
- [x] Resposta do formulário chega decodificada pelo webhook normal
- [x] Endpoint de Data Exchange criptografado funcional (round-trip testado) — Meta apenas
- [x] CRUD de Flow atrás do mesmo padrão de orquestrador multi-parceiro do envio de mensagem/Template
- [x] Front-end: listar/criar/editar/depreciar/deletar/visualizar/testar envio (partner-agnóstico)
- [x] Flow em Gupshup (create/update/publish/deprecate/delete/preview/envio testados ao vivo)
- [ ] Handler de negócio real do Data Exchange
- [ ] Flow em Evolution API

## Referências

- Meta — WhatsApp Flows (visão geral, Flow JSON, envio, Data Exchange criptografado): documentação oficial da Meta for Developers
- Gupshup — Flow Management APIs: `https://partner-docs.gupshup.io/reference/flow-management`, guia de início: `https://partner-docs.gupshup.io/docs/get-started-guide-with-gupshup-flow-management-apis`
- [ARCHITECTURE.md — Padrão orquestrador multi-parceiro](../architecture/ARCHITECTURE.md#padrão-orquestrador-multi-parceiro-coreomnimetausecase)
- Bruno: `request-collections/OmniMeta/Bruno/Flow/folder.bru` (passo a passo + pontos de atenção)

## Histórico

| Data | Nota |
| :--- | :--- |
| 2026-08-26 | Fases 1-4 implementadas (envio, CRUD, decodificação inbound, Data Exchange criptografado) + front-end completo (listagem, criação, edição, editor visual) — Meta oficial |
| 2026-08-27 | `flowId` de envio passa a ser o id local do OmniMeta (era o `flow_id` da Meta); CRUD migrado pro padrão de orquestrador multi-parceiro (`Core\OmniMeta\UseCase\Flow\*`); modal de preview local no front (substituiu link externo da Meta) |
| 2026-08-27 | Gupshup implementado: `GupshupFlowsApiService` + `Core\Gupshup\UseCase\Flow\*` (create/update/publish/deprecate/delete/preview) plugados no orquestrador; envio de Flow via Gupshup reaproveita o `ProcessFlowUseCase` do Meta sem alteração. Testado ao vivo de ponta a ponta (create → edit → publish → send) |
| 2026-08-27 | Envio simplificado: `flowActionPayload` passa a ser totalmente opcional — `ProcessFlowUseCase` usa `flow_json.screens[0].id` (tela de entrada) como padrão quando omitido, eliminando a causa mais comum do erro `(#131009)`. Descoberto ao vivo que a tela informada em `flowActionPayload.screen` só pode ser a tela de entrada, nunca uma tela real porém não-inicial |
| 2026-09-01 | Ergonomia de broker (mesmo motivo do WhatsApp Groups — ver [ARCHITECTURE.md](../architecture/ARCHITECTURE.md#ergonomia-de-broker-prefira-o-número-de-telefone-não-o-id-interno)): `Create Flow` passa a aceitar `phoneNumber` como alternativa a `whatsappAccountId` (`FlowMiddleware` resolve qualquer um dos dois, mesma convenção do `TemplateMiddleware`); `List Flows` ganhou filtro `phoneNumber`, resolvido pra `whatsappAccountId` dentro de `ListFlowsUseCase` (nunca no controller). **Achado relacionado, não corrigido nesta etapa**: `GET /templates` não tem middleware nenhum na rota — `TemplateController::index()` lê `whatsappAccount`/`phone` de `$request->attributes`, que nunca são preenchidos nesse caminho, então os filtros `whatsappAccountId`/`phoneNumber` documentados no Swagger de Templates são hoje um no-op silencioso (lista sempre tudo do client). Fora do escopo desta sessão, mas fica registrado aqui pois foi a implementação usada como referência |
