# Comunicações em lote e tracking de entrega

## Objetivo

O módulo `workflow` permite criar uma comunicação, selecionar um público, deduplicar endereços, excluir contactos suprimidos, anexar documentos privados e enviar cada mensagem de forma individual através da fila `communications`.

A implementação nunca coloca vários destinatários no mesmo campo `To`, `Cc` ou `Bcc`. Cada destinatário recebe uma mensagem própria, com conteúdo, links e identificadores de tracking independentes.

## Públicos disponíveis

- **Lista personalizada**: uma linha por endereço, nos formatos `Nome <email>`, `Nome;email` ou apenas `email`.
- **Dossiês selecionados**: resolve os responsáveis associados aos utentes desses dossiês.
- **Salas selecionadas**: resolve os responsáveis dos utentes ativos nas salas escolhidas.
- **Todos os contactos ativos**: resolve todos os responsáveis dos utentes ativos do tenant.

É possível limitar a resolução ao contacto principal ou a tipos específicos, como pai, mãe, encarregado de educação ou outro.

Antes de criar o lote, o resolver:

1. normaliza o endereço para minúsculas;
2. valida o formato;
3. remove duplicados dentro do lote;
4. consulta a lista de supressão do tenant;
5. aplica o limite máximo configurado.

Os totais excluídos por motivo ficam guardados em `audience_config.excluded`. A interface permite pré-visualizar a contagem e uma amostra antes de criar o lote. Quando o mesmo responsável surge associado a vários utentes, o envio continua único e os nomes de utentes, dossiês e salas são agregados nos placeholders singulares e plurais.

## Placeholders

Os modelos e o conteúdo direto suportam:

- `{recipient_name}`
- `{recipient_email}`
- `{tenant_name}`
- `{tenant_email}`
- `{dossier_name}`
- `{dossier_names}`
- `{dossier_reference}`
- `{student_name}`
- `{student_names}`
- `{student_number}`
- `{room_name}`
- `{room_names}`
- `{room_code}`
- `{school_year}`
- `{guardian_type}`
- `{guardian_relationship}`
- `{current_date}`
- `{current_datetime}`
- `{document_names}`
- `{unsubscribe_url}`

Os valores inseridos no HTML são escapados. O HTML do modelo é controlado pelos administradores/editor da organização e deve ser revisto antes de ser usado em produção.

## Estados

### Lote

- `draft`: criado, ainda não enviado;
- `scheduled`: agendado para o futuro;
- `queued`: colocado em fila;
- `sending`: workers a processar destinatários;
- `paused`: novos envios ficam bloqueados até retoma;
- `completed`: todos os destinatários chegaram a um estado final;
- `cancelled`: destinatários ainda pendentes foram cancelados.

### Destinatário

- `queued`, `retry`, `sending`;
- `sent`: aceite pelo transporte de email;
- `delivered`: confirmado pelo provider;
- `opened`: pixel carregado;
- `clicked`: link de tracking utilizado;
- `bounced`: rejeição pelo provider;
- `complained`: denúncia de spam;
- `unsubscribed`: cancelamento pelo destinatário;
- `failed`: falha final após as tentativas;
- `cancelled`: envio cancelado antes de ser concluído.

Os estados de interação não são regredidos. Por exemplo, um destinatário que já clicou não volta a `delivered` quando chega um evento atrasado do provider.

## Tracking de abertura

Quando ativo, é acrescentada uma imagem GIF transparente de 1 × 1 pixel com token individual. O endpoint:

- não exige sessão;
- devolve cabeçalhos `no-store`;
- regista primeiro/último acesso e contador;
- guarda apenas hashes HMAC do IP e user-agent;
- pode ignorar bots e scanners conhecidos.

Aberturas são uma indicação e não uma prova absoluta: alguns clientes bloqueiam imagens, enquanto outros fazem pré-carregamento automático.

## Tracking de cliques

Links `http` e `https` são substituídos por links individuais. Os URLs originais e tokens recuperáveis são guardados com casts encriptados, mantendo apenas hashes pesquisáveis para resolução pública.

Não são alterados links `mailto:`, `tel:`, âncoras ou protocolos não permitidos. Antes do redirecionamento, o esquema é novamente validado como HTTP/HTTPS.

## Cancelamento e lista de supressão

Quando `allow_unsubscribe` está ativo, o email recebe:

- rodapé de cancelamento;
- cabeçalho `List-Unsubscribe`;
- cabeçalho `List-Unsubscribe-Post: List-Unsubscribe=One-Click`.

O cancelamento cria uma supressão específica do tenant. Rejeições e denúncias recebidas por webhook também criam supressões. A área **Comunicações → Supressões** permite:

- pesquisar um endereço exato;
- adicionar bloqueio manual;
- libertar uma supressão;
- reativá-la.

Endereços suprimidos são excluídos antes da criação do lote e verificados novamente pelo worker imediatamente antes do envio.

## Anexos

Os documentos são selecionados dentro do tenant e anexados diretamente a partir do disco privado. São aplicados limites de:

- número de anexos;
- soma total de bytes;
- pertença ao tenant.

A mensagem não cria URLs públicas para os anexos. Um cancelamento ou pausa impede novos envios; mensagens que já tenham sido entregues ao transporte SMTP no exato momento da operação podem ainda ser processadas pelo provider.

## Filas e recuperação

Worker recomendado:

```bash
php artisan queue:work \
  --queue=communications,signatures,pdf,default \
  --tries=3 \
  --timeout=300
```

Comandos de manutenção:

```bash
php artisan communications:dispatch-scheduled
php artisan communications:maintain
```

`communications:maintain`:

- recupera destinatários presos em `sending` após interrupção do worker;
- recoloca agendamentos vencidos em fila;
- recalcula métricas;
- redistribui destinatários `queued` ou `retry`.

O scheduler executa o despacho a cada minuto e a manutenção a cada cinco minutos.

## Confirmação de entrega por provider

O transporte SMTP apenas confirma que a mensagem foi entregue ao servidor SMTP configurado. Para estados `delivered`, `bounced`, `complained`, `opened` e `clicked` do provider, deve ser configurado um webhook:

```text
POST /webhooks/communications/{provider}
```

Providers normalizados:

- `mailgun`;
- `postmark`;
- payload genérico compatível com SendGrid ou um relay próprio.

### Mailgun

Definir:

```dotenv
COMMUNICATION_PROVIDER=mailgun
MAILGUN_WEBHOOK_SIGNING_KEY=...
```

O endpoint valida `timestamp + token` com HMAC SHA-256 e rejeita pedidos com timestamp antigo.

### Postmark

Definir:

```dotenv
COMMUNICATION_PROVIDER=postmark
POSTMARK_WEBHOOK_TOKEN=segredo-longo
```

Configurar o mesmo valor no cabeçalho `X-Postmark-Webhook-Token` ou no parâmetro `?token=` do webhook.

### Provider genérico / SendGrid

Definir:

```dotenv
COMMUNICATION_WEBHOOK_SECRET=segredo-longo
```

O emissor deve enviar no cabeçalho:

```text
X-Digital-Dossier-Signature: HMAC_SHA256(raw_body, COMMUNICATION_WEBHOOK_SECRET)
```

Cada evento pode fornecer:

```json
{
  "event": "delivered",
  "event_id": "evt-123",
  "recipient_id": 42,
  "message_id": "provider-message-id",
  "email": "destinatario@example.com",
  "timestamp": 1786000000
}
```

Também são aceites arrays de eventos. `event_id` é usado para idempotência. A identificação prefere `recipient_id`, depois `message_id` e, apenas como último recurso, o envio recente para o mesmo email.

## Cabeçalhos de correlação

Cada mensagem inclui:

- `X-Digital-Dossier-Batch-ID`;
- `X-Digital-Dossier-Recipient-ID`;
- `X-Digital-Dossier-Tenant-ID`;
- `X-Mailgun-Variables`;
- `X-PM-Metadata-*`;
- `X-SMTPAPI` com `unique_args`.

Estes valores permitem que providers compatíveis devolvam o ID do destinatário no webhook.

## Exportação e auditoria

A página do lote mostra métricas e destinatários. O CSV inclui:

- nome e email;
- tipo de contacto;
- estado final;
- datas de envio, entrega, primeira abertura e primeiro clique;
- contadores;
- erro final.

A tabela `communication_events` preserva eventos idempotentes e metadados técnicos. Operações administrativas relevantes também são registadas no log de auditoria da plataforma.

## Privacidade e conformidade

Tracking de abertura e clique pode exigir informação prévia ou outra base legal, dependendo do contexto e da jurisdição. Cada tenant deve poder desativá-lo por lote e refletir a prática na respetiva política de privacidade.

Os webhooks devem ser servidos exclusivamente por HTTPS. Segredos de webhook nunca devem ser incluídos no repositório.
