notification-flow

# Notification Flow

## Objetivo

Explicar o fluxo canonico de criacao e execucao de notificacoes no Communications Service.

## Contrato recomendado

Quem produz notificacoes nao deve escrever diretamente em:

- `notifications`
- `notification_recipients`
- `notification_deliveries`

O contrato recomendado agora e:

1. um produtor confiavel, como `n8n`, chama `POST /internal/notifications`
2. o Communications Service valida o payload
3. aplica `notification_preferences`
4. cria `notifications`
5. cria `notification_recipients`
6. cria `notification_deliveries`
7. enfileira imediatamente o que ja estiver elegivel

## Navegacao

`navigationKey` passa a ser a identidade estavel de navegacao da notificacao.

Regras:

- o produtor pode enviar `navigationKey`
- se nao enviar, o backend gera automaticamente
- `route` deixa de ser a origem da navegacao
- o backend sempre persiste `route` como:
  - `/dashboard/notifications/r/<navigationKey>`
- se `payload.ui.copyOnOpen` vier com:
  - `message`
  - `body`
  - `phone`
  o backend anexa `?copy=<valor>` na rota derivada

Na pratica:

- `navigationKey` identifica a notificacao no front
- `route` vira apenas a URL resolvedora persistida
- `dedupeKey` continua sendo so deduplicacao
- `groupKey` continua sendo so agrupamento
- para WhatsApp, envie `payload.links.whatsAppShareUrl`
- para os botoes do push, use `actions` com `action` e `title`

Exemplo de lead handoff com WhatsApp:

```json
{
  "actions": [
    { "action": "copy-handoff", "title": "Copiar mensagem" },
    { "action": "open-whatsapp", "title": "Abrir WhatsApp" },
    { "action": "open-sheet", "title": "Abrir planilha" }
  ],
  "payload": {
    "kind": "lead_handoff",
    "ui": {
      "copyOnOpen": "body"
    },
    "links": {
      "whatsAppShareUrl": "https://wa.me/5511999999999?text=...",
      "sheetUrl": "https://docs.google.com/spreadsheets/d/..."
    },
    "push": {
      "body": "Novo lead de Taboao da Serra - SP"
    }
  }
}
```

O front continua consumindo o PocketBase como antes.

## Quem chama essa API

Chamadores esperados:

- `n8n`
- backend interno
- workflows trusted
- servicos internos

Nao e uma API pensada para o browser chamar diretamente, porque ela exige:

- `Authorization: Bearer <SERVICE_AUTH_TOKEN>`

## O que a API faz

Quando recebe uma requisicao valida, o servico:

- trata a criacao como uma unica operacao
- usa `dedupeKey` ou `idempotencyKey` quando enviados
- cria a notificacao inicialmente como `draft`
- cria recipients e deliveries necessarios
- faz compensacao em caso de falha durante a montagem
- muda a notificacao para `queued` quando houver deliveries
- muda a notificacao para `cancelled` quando tudo for suprimido por preferencia

Na pratica, isso elimina a necessidade de o consumidor conhecer 3 tabelas.

## Audiencia e scopes

O servico suporta os 3 scopes persistidos em `notifications`:

- `user`: notifica usuarios explicitos em `recipients`
- `instance`: notifica usuarios de uma instancia, com opcao de filtrar por role
- `application`: notifica usuarios ligados a instancias na plataforma, com opcao de filtrar por role

Mesmo para `scope=instance` e `scope=application`, o Communications Service cria `notification_recipients` para cada usuario resolvido. Isso e necessario porque as regras de leitura do PocketBase dependem de `notification_recipients`.

Para resolver audiencia automaticamente, use `audience.roles`:

```json
{
  "scope": "instance",
  "instanceId": "4f0kfmefs6x09lo",
  "audience": {
    "roles": ["super_admin", "gestor", "notificacoes"]
  }
}
```

Regras:

- se `recipients` vier preenchido, os usuarios explicitos sao mantidos
- se `audience` vier junto, usuarios resolvidos sao mesclados e duplicados sao removidos
- `scope=instance` exige `instanceId` e busca em `user_instance_roles.instance`
- `scope=application` busca em `user_instance_roles` sem filtrar instancia
- `scope=user` continua exigindo `recipients`

## Como `notification_preferences` entra no fluxo

`notification_preferences` e interpretado no proprio Communications Service, nao no front.

Em ambientes com os hooks externos do PocketBase habilitados, preferencias basicas sao provisionadas automaticamente quando usuarios e vinculos em instancias sao criados. Esse provisionamento vive fora deste repositorio e esta documentado em:

- `docs/concepts/pocketbase-notification-preferences-hooks.md`

Regras suportadas na ingestao:

- `in_app_enabled` controla a disponibilidade do item na experiencia in-app
- `toast_enable` controla especificamente o canal `realtime` usado para toast/sinalizacao imediata
- `push_enabled` controla o canal `push`
- `email_enabled` controla o canal `email`
- `whatsapp_enabled` controla o canal `whatsapp`
- `mute_low_priority` suprime notificacoes `low`
- `require_action_push` permite push apenas quando `requireAction = true`
- `mute_until` suprime envios ate a data configurada

O que ainda nao esta sendo interpretado na ingestao:

- `quiet_hours`

Esse ponto continua documentado como evolucao futura.

Observacao: para seguranca operacional, `whatsapp` exige `whatsapp_enabled=true`. Quando nao ha preferencia provisionada para o usuario/escopo, o canal `whatsapp` e suprimido.

## Trigger operacional

O servico tem 2 modos de disparo:

### 1. Imediato

Ao criar pela API interna, deliveries sem agendamento futuro ja sao enfileiradas na hora.

### 2. Scan periodico

Mesmo sem trigger imediato, os workers continuam consultando o PocketBase em intervalo fixo.

Padrao atual:

- `NOTIFICATION_SCAN_EVERY_MS = 30000`

Ou seja:

- ate 30 segundos para deliveries `queued`
- retries respeitam `next_retry_at`

## Fluxo resumido

1. `n8n` chama `POST /internal/notifications`
2. o servico persiste tudo no PocketBase
3. o servico ja enfileira deliveries elegiveis
4. workers processam por canal/provider
5. `notification_deliveries`, `notification_recipients` e `notifications` sao atualizadas

## Providers externos

### Email com Resend

Para usar Resend, envie `channel=email` e `provider=resend`.

Quando `RESEND_API_KEY` estiver configurado, `provider=resend` e assumido automaticamente para deliveries criadas pela API canonica com `channel=email`.

O provider tenta resolver o email pelo usuario do PocketBase usando `PB_USER_EMAIL_FIELD` e fallback para `email`. O payload pode sobrescrever o destino com `payload.email.to`.

Exemplo:

```json
{
  "channels": ["email"],
  "providers": {
    "email": "resend"
  },
  "payload": {
    "email": {
      "from": "Ailian <[email protected]>",
      "subject": "Novo lead recebido",
      "html": "<p>Um novo lead chegou.</p>"
    }
  }
}
```

Para templates de email versionados neste repositorio, use `payload.email.template`.
O Communications Service renderiza `subject`, `html` e `text` localmente antes
de chamar o Resend. Não use `templateId` para esses templates.

Templates disponiveis:

- `payment-approved`: confirmação de pagamento aprovado
- `invoice-issued`: aviso de nota fiscal emitida

Exemplo de pagamento aprovado:

```json
{
  "channels": ["email"],
  "providers": {
    "email": "resend"
  },
  "payload": {
    "email": {
      "template": "payment-approved",
      "subject": "Pagamento aprovado",
      "variables": {
        "appName": "Ailian",
        "customerName": "Marina Costa",
        "orderId": "PED-2026-0001",
        "productName": "Ghostwriter Starter",
        "paymentAmount": "R$ 199,90",
        "paymentMethod": "Cartao de credito",
        "paidAt": "11/05/2026 18:30",
        "appUrl": "https://app.ailian.com.br/billing"
      }
    }
  }
}
```

Exemplo de nota fiscal emitida:

```json
{
  "channels": ["email"],
  "providers": {
    "email": "resend"
  },
  "payload": {
    "email": {
      "template": "invoice-issued",
      "subject": "Nota fiscal emitida",
      "variables": {
        "appName": "Ailian",
        "customerName": "Marina Costa",
        "invoiceNumber": "NF-e 1024",
        "productName": "Creditos Ailian 20K",
        "invoiceAmount": "R$ 199,90",
        "issuedAt": "11/05/2026 18:45",
        "invoiceUrl": "https://app.ailian.com.br/invoices/nf-001",
        "appUrl": "https://app.ailian.com.br/billing"
      }
    }
  }
}
```

A lista atual tambem fica disponivel em:

- `/docs/email-templates`
- `/docs/email-templates.json`

### WhatsApp com Chatwoot

Para usar WhatsApp em notifications, envie `channel=whatsapp` e `provider=chatwoot`.

`provider=chatwoot` e assumido automaticamente para deliveries criadas pela API canonica com `channel=whatsapp`.

Regras:

- `whatsapp_enabled` precisa estar `true` na preferencia escolhida do usuario
- sem preferencia provisionada, `whatsapp` e suprimido
- o provider tenta resolver o telefone em `profiles` usando `PB_PROFILE_CALLING_CODE_FIELD` + `PB_PROFILE_PHONE_FIELD`
- se nao houver profile/telefone, tenta fallback no usuario do PocketBase usando `PB_USER_WHATSAPP_FIELD`
- tambem aceita override por `payload.whatsapp.phoneNumber` ou `payload.targets.whatsapp.phoneNumber`

Exemplo com template WhatsApp:

```json
{
  "channels": ["realtime", "push", "whatsapp"],
  "providers": {
    "push": "webpush",
    "whatsapp": "chatwoot"
  },
  "payload": {
    "whatsapp": {
      "templateName": "novo_lead_recebido",
      "language": "pt_BR",
      "processedParams": {
        "lead_name": "Fulano",
        "city": "Sao Paulo"
      }
    }
  }
}
```

Se ja existir uma conversa ou contato no Chatwoot, o payload pode informar:

```json
{
  "payload": {
    "whatsapp": {
      "conversationId": 123,
      "contactId": 456
    }
  }
}
```

## Exemplo real usado nas docs

Os exemplos interativos do Scalar usam hoje:

- `instanceId = 4f0kfmefs6x09lo`
- `userId = y1412767403q7jx`

Services usados nos exemplos:

- `z97bsc1rlv9qbk3` para chatbot/ConectAi
- `51x9x66y6z86h8w` para prospeccao ativa
- `x4q5cd5zj5qtwi3` para geracao de conteudo