* feat: capturar origem de marketing do briefing de contato (MAN-105)
Adiciona tracking simples da origem (UTM ou referrer, primeiro-toque
vence) para satisfazer WEB-05: a origem viaja apenas com o e-mail
interno do briefing, sem Lead model, tabela ou superfície de CRM
(Fase 2 fora de escopo). Middleware no grupo "web" cobre só as rotas
públicas (o painel Filament tem sua própria stack de middleware);
lógica de captura/sanitização isolada em App\Domain\Contact para não
poluir controller nem view.
Fail-open por construção: qualquer falha ao capturar ou ler a origem é
logada sem dados pessoais e a submissão segue normalmente. Respeita
LGPD (§12.5): nada além da origem é guardado, valores são truncados
para não inflar a sessão, e um referrer apontando para o próprio host
é descartado por não ser sinal de marketing. Valores não confiáveis
(query string, referrer) são sanitizados antes de entrar na sessão e
no e-mail em HTML.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): capturar apenas a origem do referrer, não a URL completa
O fallback de referrer em MarketingOrigin::capture() armazenava a URL
inteira (incluindo query string e fragmento), não apenas o site de
origem. Um link de campanha de e-mail personalizado (ex.:
?email=...&subscriber_id=...) chegava intacto à sessão (persistida em
banco via SESSION_DRIVER=database) e ao corpo do e-mail de briefing
lido por um humano — indo além do que SPEC.md WEB-05 pede ("origem de
marketing") e do §12.5 ("coletar apenas dados necessários").
Agora o referrer é reduzido a esquema+host antes de sanitizar,
descartando path, query string e userinfo.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* test(contato): cobrir first-touch quando a navegação seguinte traz UTM concorrente
test_first_touch_wins_when_later_navigation_has_no_utm não exercitava
de fato a guarda "primeira captura vence" em CaptureMarketingOrigin —
as duas navegações seguintes não carregavam nenhum sinal (nem UTM, nem
referrer), então MarketingOrigin::capture() já retornava [] e o
early-return por ausência de sinal (não a guarda de sessão) é quem
impedia a sobrescrita. Confirmado removendo a guarda: a suíte
continuava passando.
Este teste novo usa uma segunda navegação com UTM diferente
(utm_source=facebook), o que só passa se a guarda de sessão estiver
ativa — mutação verificada manualmente antes de escrever a asserção.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): não capturar origem em sitemap.xml e robots.txt
O middleware estava anexado ao grupo `web` inteiro, que também cobre
`/sitemap.xml` e `/robots.txt`. Um crawler chegando em
`/sitemap.xml?utm_source=...` consumia o slot de primeiro toque com
tráfego que nunca vai enviar um briefing, e ainda iniciava sessão em uma
rota técnica.
As duas rotas passam a ser ignoradas por nome, com teste cobrindo que
elas não gravam nada na sessão e que uma visita humana seguinte ainda é
capturada normalmente.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
Publish immutable FrankenPHP images to GHCR, auto-deploy staging after CI,
and promote the same digest to production with smoke, backup, and rollback docs.
Co-authored-by: Cursor <cursoragent@cursor.com>