# Relatórios · PDF e envio por e-mail

Como o PDF e o e-mail funcionam, e o que fazer no servidor para ligar.

## Como funciona

```
aprovar (ou aprovação automática)
  └─► RenderReportPdfJob          fila reports-pdf · worker próprio · 2 processos
        gera link assinado do documento (10 min, só relatório aprovado)
        PDFShift abre a página, espera window.reportIsReady() e devolve o PDF
        grava em storage/app/private/reports/{projeto}/{relatorio}.pdf
        └─► DeliverReportJob       fila reports-mail
              uma linha em report_deliveries por destinatário
              └─► SendReportEmailJob   fila reports-mail · 1 por destinatário
                    Resend com fallback pro SMTP, PDF anexo, logo embutido (CID)
```

- **O PDF é a própria página aprovada** (`Relatorios/Documento` em modo PDF), não uma cópia.
- **Imagens entram em base64** no modo PDF (`ReportAssetInliner`): logos, foto do autor da nota
  e miniaturas do Meta. Imagem que falha cai no fallback do relatório (iniciais, SVG da Exent).
- **Ritmo**: `REPORTS_PDF_RATE_PER_MINUTE` e `REPORTS_MAIL_RATE_PER_MINUTE`. Job barrado pelo
  limite volta pra fila sem gastar tentativa.
- **Falhas**: rede/5xx tentam de novo; 429 espera o `Retry-After` da API; chave inválida ou sem
  crédito para na hora e aparece na listagem. Ver "Quando o PDFShift cai" abaixo.
- **Ninguém recebe em dobro**: quem já está `sent` em `report_deliveries` é pulado; "Reenviar"
  só alcança quem falhou.
- **Modo automático** (config de envio do projeto): só o relatório agendado se aprova sozinho.
  Emissão avulsa e "gerar novo" continuam pendentes.

## Ligar no servidor (uma vez)

### 1. Migrations

```bash
cd /var/www/projetos5.exentdev.com.br/st/8.3/dashboard
php artisan migrate --force
```

### 2. Provedor de e-mail

Fica na tela: **Configurações → Tokens**, cartão **SMTP** e cartão **Resend**. Essa é a
configuração GERAL. Cada empresa pode ter a sua própria, na aba **Tokens** de
Configurações → Empresa: aí o cliente da Vendrix recebe de um remetente da Vendrix, e o
geral só entra se o da empresa falhar. Os dois podem
ficar ligados ao mesmo tempo, e não são mais mutuamente exclusivos: com os dois ativos, o
**Resend é tentado primeiro em todo envio**, e o **SMTP entra sozinho no lugar dele** se o
Resend lançar erro (chave inválida, domínio não verificado, API fora do ar). Não precisa
reenviar nada à mão — a próxima tentativa (ou o próximo destinatário) já sai pelo SMTP.

Cada cartão tem **"Enviar e-mail de teste"**, que sai na hora (sem fila), mostra o erro exato
do provedor caso ele recuse, e diz por qual provedor o teste realmente saiu.

Com os dois desligados, o envio cai no `.env` do servidor (`MAIL_MAILER`, `MAIL_HOST`,
`MAIL_SCHEME`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM_ADDRESS`) — é o que vale
enquanto ninguém configurar a tela, e o que faz a migração não quebrar nada.

No Gmail/Google Workspace a senha é uma **senha de app** (Conta Google → Segurança →
Senhas de app), e a conta precisa ter verificação em duas etapas.

No Resend, o domínio do remetente precisa estar verificado no painel deles.

Opcionais no `.env` (os valores abaixo já são o padrão):

```dotenv
REPORTS_PDF_RATE_PER_MINUTE=20
REPORTS_MAIL_RATE_PER_MINUTE=30
# Teste sem gastar crédito (PDF sai com marca d'água):
PDFSHIFT_SANDBOX=false
```

Depois de mexer no `.env`:

```bash
php artisan config:cache
```

### 3. Workers (sudo)

Conferir o usuário dos workers que já existem e ajustar `user=` no arquivo se for outro:

```bash
grep -h "^user" /etc/supervisor/conf.d/*.conf
```

Instalar:

```bash
sudo cp deploy/supervisor/exent-reports.conf /etc/supervisor/conf.d/exent-reports.conf
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status | grep exent-reports
```

Devem aparecer `exent-reports-pdf_00`, `exent-reports-pdf_01` e `exent-reports-mail_00` como
`RUNNING`.

### 4. Chave do PDFShift

Configurações → Tokens → **PDFShift** → colar a chave → Salvar.

A marca de cada empresa (logo da capa e gradiente) fica em Configurações → Empresa, na
aba **Relatórios**.

## Testar

1. Deixar um projeto de teste só com o seu e-mail nos destinatários.
2. Aprovar um relatório dele. A listagem mostra "Gerando PDF" → "Enviando" → "Enviado".
3. Conferir o PDF pelo botão de baixar e o e-mail na caixa.

## Para onde volta a resposta do cliente

O remetente dos relatórios é um endereço do provedor de envio (hoje o Resend) que **não recebe
nada**. Até 10/2026 o `reply_to` apontava pra ele mesmo: quem respondia um relatório falava com
uma caixa inexistente, e a mensagem se perdia em silêncio.

Agora o `reply_to` depende de **quem mandou** (`App\Mail\ReportMail::replyToAddress()`):

| Origem do envio | `reply_to` | Rodapé do e-mail |
|---|---|---|
| Aprovado por uma pessoa (`report_runs.approved_by` preenchido) | e-mail de quem aprovou | "É só responder este e-mail, que chega direto para Felipe." |
| Envio automático (`approved_by` nulo) | caixa de não-resposta | "Este é um envio automático · por favor, não responda este e-mail." |

O e-mail de quem aprovou é lido do cadastro **no momento do envio**, e não congelado junto com a
marca e a nota: um reenvio de um relatório antigo deve chegar no endereço atual da pessoa, não no
que ela tinha na época. Usuário apagado, ou sem e-mail no cadastro, cai no mesmo caminho do envio
automático.

### Resposta automática dos envios automáticos (Resend Inbound)

O Resend é só saída. Para a caixa de não-resposta conseguir devolver o "esta caixa não recebe
respostas", ela precisa do Resend Inbound:

1. No painel do Resend, habilitar **Inbound** no domínio e criar o endereço (ex.:
   `naoresponda@exent.com.br`).
2. Apontar o **MX** do domínio pro Resend, como o painel indicar.
3. Criar um **webhook** pro evento `email.received` apontando para
   `https://<host>/webhooks/resend/inbound`, e copiar o segredo de assinatura (`whsec_...`).
4. No `.env` do servidor:

```bash
REPORTS_NO_REPLY_ADDRESS=naoresponda@exent.com.br
REPORTS_NO_REPLY_NAME="Exent"

REPORTS_INBOUND_ENABLED=true
REPORTS_INBOUND_SIGNING_SECRET=whsec_...
REPORTS_INBOUND_CONTACT_EMAIL=relatorios@exent.com.br   # opcional: pra onde a pessoa deve escrever

# Opcionais, com padrão razoável:
# REPORTS_INBOUND_API_KEY=re_...          # vazio usa a chave do Resend de Configurações · Tokens
# REPORTS_INBOUND_COOLDOWN_HOURS=24
```

5. `php artisan config:clear` e garantir que há worker na fila `reports-mail` (a mesma do envio).

**Só o `REPORTS_NO_REPLY_ADDRESS` já resolve metade do problema**: o aviso no corpo aparece e a
resposta vai pra uma caixa de verdade. O `REPORTS_INBOUND_*` é o que faz o cliente receber a
confirmação automática em vez de silêncio.

Sem `REPORTS_INBOUND_SIGNING_SECRET`, o endpoint responde 503 e não processa nada. É de
propósito: um endereço público que dispara e-mail sem conferir quem chamou é um canhão de spam
de graça, com a nossa reputação de domínio no prejuízo.

**Pingue-pongue de robôs** é o risco real de qualquer respondedor automático: o "fora do
escritório" do cliente responde o nosso aviso, que responde de novo, e os dois ficam
conversando. Três travas em `SendNoReplyAutoReplyJob`:

- **Cabeçalhos de automação na entrada:** `Auto-Submitted` diferente de `no`, `Precedence:
  bulk/list/junk/auto_reply`, `List-Id`, `X-Autoreply`, `Return-Path: <>`. Qualquer um desses e
  não respondemos. Eles vêm de uma segunda chamada à API (`GET /emails/receiving/{id}`) — o
  webhook em si traz só metadados.
- **Cabeçalhos de automação na saída:** a nossa resposta sai com `Auto-Submitted: auto-replied`,
  `X-Auto-Response-Suppress: All` e `Precedence: auto_reply`, pra quem estiver do outro lado se
  calar. (Foi por isso que o `ResendTransport` passou a repassar cabeçalhos próprios no payload
  — antes ele descartava todos em silêncio.)
- **Teto por remetente:** uma resposta a cada 24h por endereço, e deduplicação pelo `svix-id` do
  webhook (uma tentativa reenviada traz o mesmo id).

## Rastreio de abertura

Todo e-mail de relatório leva um pixel de 1x1 invisível. Quando o cliente de e-mail busca essa
imagem no servidor, `report_deliveries.opened_at` grava o horário da primeira vez, e
`open_count` soma toda reabertura. Na listagem, passar o mouse no selo "Enviado" mostra "X de Y
abriram".

**É estimativa, não prova de leitura** — duas distorções conhecidas, das mais comuns em qualquer
sistema de pixel de abertura:

- **Subconta** (falso negativo): Outlook corporativo costuma bloquear imagem remota por padrão.
  Alguém pode ter lido o e-mail inteiro sem a imagem nunca ser buscada.
- **Superconta** (falso positivo): o Apple Mail (iCloud, e boa parte do iPhone/Mac) pré-carrega
  a imagem de toda mensagem pelo proxy deles assim que ela chega na caixa, então esse
  destinatário aparece como "abriu" mesmo sem ter lido nada.

Serve pra um sinal direcional ("esse cliente nunca abre, vale ligar?"), não pra medir taxa de
abertura com precisão.

O reenvio de teste (`reports:previa-email`) não conta como abertura de propósito: a cópia
de conferência não carrega o pixel, só o e-mail que de fato sai pelo `SendReportEmailJob`.

## Quando o PDFShift cai

Três camadas, da mais imediata para a mais lenta. Nenhuma exige clicar em nada.

**1. O próprio job tenta de novo.** Falha de rede, timeout ou 5xx viram nova tentativa, com
espera crescente: 1min, 5min, 15min, 30min, 1h, 1h. Um relatório atravessa sozinho umas duas
horas de instabilidade. O 429 é tratado à parte: espera o `Retry-After` que a API mandou e não
gasta tentativa.

**2. A repescagem, de hora em hora.** `reports:repescar-pdf` (agendado, aos 20 minutos) recoloca
na fila todo PDF que ficou em erro. É o que cobre uma queda mais longa e, principalmente, o
"acabou o crédito na segunda, recarreguei na terça": ao voltar o crédito, os relatórios se
resolvem sozinhos na próxima passada.

Um relatório é repescado no máximo `REPORTS_PDF_RETRY_MAX` vezes (6) e só enquanto tiver sido
aprovado nos últimos `REPORTS_PDF_RETRY_WINDOW_DAYS` dias (7). O teto existe para um relatório
que nunca vai renderizar não ficar sendo tentado para sempre.

Rodar à mão:

```bash
php artisan reports:repescar-pdf --dry-run     # mostra o que faria
php artisan reports:repescar-pdf               # repesca agora
php artisan reports:repescar-pdf --report=UUID # força um específico, sem teto
```

**3. O aviso por e-mail.** Um e-mail por passada, com a lista do que falhou desde a última, para
o endereço em `REPORTS_ALERT_EMAIL`. É uma lista só e não um e-mail por relatório: numa queda,
oitenta relatórios falham juntos.

```dotenv
REPORTS_ALERT_EMAIL=andreza.lima@exent.com.br
```

Sem essa variável o aviso não sai (o resto continua funcionando).

> Clicar em "Gerar o PDF" na listagem zera o contador de repescagens e libera um aviso novo: é
> uma pessoa decidindo recomeçar. A repescagem automática não zera nada, senão mandaria um
> e-mail por hora sobre a mesma falha.

**Plano B manual:** o documento é uma página de verdade. O botão "Imprimir", dentro de "ver
relatório", abre a versão de impressão e dá para salvar em PDF pelo navegador, sem depender do
PDFShift. Não gera o anexo do e-mail automático, mas tira do zero num dia ruim.

## Quando algo dá errado

| Na listagem | Causa provável | O que fazer |
|---|---|---|
| "Sem PDF" | Chave do PDFShift não cadastrada | Cadastrar em Tokens e usar "Gerar o PDF" |
| "PDF com erro" (passe o mouse) | Chave inválida, sem créditos, página não abriu | Corrigir e usar "Gerar o PDF" |
| Fica em "Gerando PDF" | Worker `exent-reports-pdf` parado | `sudo supervisorctl status` |
| Fica em "Enviando" | Worker `exent-reports-mail` parado | `sudo supervisorctl status` |
| "Envio falhou"/"parcial" | Provedor(es) recusaram (senha, limite, domínio). Com Resend e SMTP ativos, os dois falharam -- o fallback já tentou o segundo sozinho | Usar "Enviar e-mail de teste" em Tokens, corrigir, depois "Reenviar" |
| "Sem destinatários" | Projeto sem usuários nem e-mails extras | Cadastrar em Configuração de relatórios do projeto |

Logs dos workers: `storage/logs/worker-reports-pdf.log` e `storage/logs/worker-reports-mail.log`.
