Commit Graph

9 Commits

Author SHA1 Message Date
fc1c6177d6 feat: remodelar /sobre conforme mock editorial
All checks were successful
CI / static (push) Successful in 2m21s
CI / unit (push) Successful in 3m17s
CI / feature (push) Successful in 2m29s
CI / container (push) Successful in 18s
CI / browser (push) Successful in 3m8s
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
2026-08-12 20:34:13 +00:00
a0963dd57a feat: páginas de detalhe das modalidades de assessoria 2026-08-12 09:18:57 -03:00
d0e508b237 feat: reestruturar jornada pública de casamentos (#52) 2026-08-11 15:47:43 -03:00
7e68c0e37f chore: fechar portões de qualidade da Fase 0 (MAN-120) (#39)
* feat: gates de qualidade da Fase 0 (auth, npm audit, cobertura, compose local)

Implementa os itens pendentes de SPEC.md §18 mapeados pela mudança OpenSpec
complete-foundation-parity (grupos 1, 2 e 3 de tasks.md):

- MustVerifyEmail em User, com canAccessPanel exigindo is_active E
  hasVerifiedEmail(). Seeds de admin/assistant local passam a marcar
  email_verified_at. Verificação de e-mail permanece administrada apenas
  pelo admin (seed/edição) — não há rota de auto-verificação registrada
  (->emailVerification() não foi chamado) — decisão deliberada, já que
  usuários são criados pelo admin, não se autocadastram.

- Reset de senha seguro: página customizada
  App\Filament\Pages\Auth\RequestPasswordReset substitui a página padrão do
  Filament, que vaza a existência de contas via notificação de erro
  distinguível em dois casos — Password::INVALID_USER (e-mail inexistente)
  e Password::RESET_THROTTLED (só ocorre para usuário existente, quando um
  token foi criado há pouco). Ambos os casos agora respondem com a mesma
  notificação de sucesso.

- declare(strict_types=1) adicionado aos 15 arquivos de app/ que ainda não
  tinham: AdminPanelProvider, Controller e 13 páginas de Filament Resources.

- npm audit adicionado ao script `quality` do composer e ao job `static` do
  CI, com --audit-level=high (documentado inline no workflow). O gate é hoje
  vazio na prática — package.json não tem `dependencies` de runtime — mas
  protege contra regressões futuras.

- Gate de cobertura Domain/Application >= 80% no job `unit` do CI. Escopo
  via phpunit.coverage.xml dedicado (o phpunit.xml global continua cobrindo
  todo app/ para os demais usos). O job `unit` ganhou serviço de postgres e
  passou a rodar Unit+Architecture+Feature juntas, porque as classes de
  App\Application\Queries\Marketing só são exercitadas por testes Feature
  hoje — um gate baseado só em Unit ficaria bem abaixo de 80%. Medido
  localmente com pcov: 98,1% (nenhum teste novo foi necessário para atingir
  o limiar).

- Serviço `app` (FrankenPHP) no docker-compose.yml para paridade local com
  staging/produção, dependente de postgres saudável, com DB_HOST/DB_PORT
  sobrescritos para resolver o serviço postgres pelo nome (o padrão
  127.0.0.1 do .env só funciona para processos no host). Validado com um
  smoke isolado (projeto/portas dedicados via overlay `!override`, sem
  tocar o container amare-postgres compartilhado por outra worktree):
  depends_on/healthcheck funcionou, `php artisan migrate` rodou dentro do
  container via DB_HOST=postgres, e /up respondeu 200. README atualizado
  (PHP 8.4 canônico, composer.json mantém ^8.3 por design).

Fora do escopo: deploy hello-world em staging (Dokploy com falha, item de
infra não relacionado a este change) — L2353 e o critério de saída da Fase
0 continuam sem marcar. Também ficam pendentes, por dependerem de uma
execução real de CI/PR (task 3.4, 6.1, 6.2 do OpenSpec): prova de que o CI
falha com quebra intencional de audit/cobertura, e evidência de deploy/
rollback em staging.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix: fecha lockout de admin-criado e documenta pcov/compose no auth de usuários

Correção de acompanhamento ao commit anterior (gates de qualidade da Fase 0),
achada por revisão adicional após o primeiro commit:

- CreateUser::handleRecordCreation agora define email_verified_at ao criar um
  usuário pelo painel. Sem isso, UserForm não expõe esse campo (nem está no
  #[Fillable] de User — de propósito, para nunca virar mass-assignable via
  formulário), então todo usuário criado pelo admin nascia com
  email_verified_at nulo e, com canAccessPanel() agora exigindo e-mail
  verificado, ficava trancado para sempre — sem rota de auto-verificação e
  sem recuperação via reset de senha (o callback do Filament pula o envio
  para quem falha canAccessPanel(), mas ainda reporta sucesso). Usa
  forceFill() (não atribuição direta, que o PHPStan rejeita pelo tipo
  Carbon/string do cast) para contornar o guard de mass assignment só nesse
  ponto. Regressão coberta por teste que cria um usuário via
  Livewire::test(CreateUser::class) — não pela factory, que não passa pelo
  mesmo caminho — e confirma login em seguida; teste falha sem a correção
  (verificado manualmente revertendo e rodando de novo).

- Confirmado que DatabaseSeeder::run() (commit anterior) já funciona sem
  ajuste: User::query()->updateOrCreate() pareceria sofrer o mesmo guard de
  #[Fillable], mas `php artisan db:seed` executa dentro de
  Model::unguarded() (Illuminate\Database\Console\Seeds\SeedCommand), o que
  levanta o guard durante o seed inteiro. Adicionado teste de regressão que
  roda via $this->seed(DatabaseSeeder::class) (o mesmo caminho de
  `composer setup`) para travar esse comportamento — chamar a mesma lógica
  fora do comando db:seed reproduz o guard bloqueando o campo, confirmando
  que a dependência do unguard() é real, não coincidência.

- README: nota de que `docker compose up -d postgres` agora exige o `.env`
  do passo 1, porque o serviço `app` referencia `.env` via `env_file` e o
  Compose valida o arquivo inteiro antes de subir qualquer serviço.

- AGENTS.md: documentado `composer test:coverage` (usado pelo job `unit` do
  CI) e que `docker compose up -d` sem argumento também sobe o serviço `app`
  agora, não só o Postgres.

Nota para quem for escrever o próximo teste de recurso Filament neste repo:
Livewire::test(...)->fillForm([...]) é um no-op silencioso aqui —
app()->runningUnitTests() retorna false no setup de testes deste projeto,
então o guard de Filament\Schemas\Concerns\InteractsWithSchemas::
fillFormDataForTesting() nunca aplica o estado, e o teste falha depois com
erros de validação "required" que parecem um bug no formulário, não no
teste. Use ->set('data.campo', valor) (como os testes de Login já fazem)
até isso ser investigado — fora do escopo deste change.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix(auth): verificar usuários existentes ao adotar MustVerifyEmail

O `canAccessPanel` passou a exigir `hasVerifiedEmail()`, mas nenhuma
migration preenchia `email_verified_at` para contas que já existem. Toda
conta criada antes desta mudança tem o campo nulo e ficaria trancada fora
do /admin no instante do deploy.

E não haveria volta pelo aplicativo: nenhuma rota de verificação
self-service é registrada, e o fluxo de reset de senha deliberadamente
não notifica quem falha no `canAccessPanel` — ainda reportando sucesso.
Recuperar exigiria shell no contêiner.

Verificar essas contas é o padrão correto, não um atalho. Não existe
cadastro público: toda conta existente foi criada por um admin, pelo
painel ou pelo snippet de tinker do runbook de deploy. Esse ato é a
verificação — exatamente o raciocínio que o CreateUser aplica às contas
criadas de agora em diante.

A verificação é datada pelo `created_at` da conta, não pelo momento em
que a migration roda, para não inventar um histórico. O `down()` é um
no-op documentado: anular as colunas recriaria justamente a falha que
esta migration existe para evitar, e a divisão original entre nulos e
não-nulos não está registrada em lugar nenhum.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 10:48:04 -03:00
37296f758f feat: capturar origem de marketing do briefing de contato (MAN-105) (#37)
* 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>
2026-08-10 09:12:26 -03:00
eed8240487 fix(contato): normalizar e-mail e telefone no formulário público de briefing (#31)
* fix(contato): normalizar e-mail e telefone no formulário público de briefing

SPEC.md §12.3 exige normalização de e-mail e telefone no formulário
público, mas o ContactBriefingRequest só validava, sem normalizar
(MAN-104).

E-mail: aplica trim + lowercase em prepareForValidation — evita
duplicatas triviais ("Maria@X.com" vs "maria@x.com") e mantém a
detecção de duplicidade por hash consistente.

Telefone: normaliza para o formato brasileiro legível
"(DD) 9XXXX-XXXX"/"(DD) XXXX-XXXX", extraído para o value object
App\Domain\Contact\BrazilianPhoneNumber por concentrar lógica de
decisão (contagem de dígitos, remoção de DDI 55) que merece teste
isolado. Optou-se pelo formato com máscara em vez de dígitos puros
porque o Mailable ContactBriefing e seus templates (html/text) apenas
imprimem o valor do campo "Telefone/WhatsApp" em uma tabela/lista,
sem link "tel:" nem formatação na view — "(11) 98888-7777" é o que
fica legível para quem recebe o briefing por e-mail, enquanto
"11988887777" é opaco de bater o olho.

Para não descartar informação que o destinatário precisa, o
normalizador só reformata quando a string é composta exclusivamente
por caracteres de telefone; anotações como "(WhatsApp)" ou "falar com
João" junto do número são preservadas como estão. Entradas que não
batem com 10/11 dígitos (após remover DDI) também são preservadas,
apenas com espaços internos colapsados.

Honeypot, rate limit e aceite de privacidade não foram tocados.

Cobertura: teste de unidade para o value object (formatos válidos,
DDI, anotações, formatos não reconhecidos) e teste de feature
provando que uma submissão com e-mail maiúsculo/padded e telefone
"sujo" chega normalizada nos Mailables ContactBriefing e
ContactBriefingConfirmation.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix(contato): evitar fabricar DDD/9º dígito em números não brasileiros

O normalizador aceitava qualquer string de 10 ou 11 dígitos como se
fosse um telefone brasileiro, sem checar se os dois primeiros dígitos
formam um DDD plausível (11-99, nunca com zero em nenhuma posição) ou
se um número de 11 dígitos tem o 9º dígito obrigatório do celular.
Isso fazia números estrangeiros como '2025551234' (EUA) virarem
'(20) 2555-1234' — um número brasileiro plausível, porém inventado,
que destrói silenciosamente o contato real na tabela do e-mail de
briefing. Um '+55' explícito sem DDD (ex.: '+55 98888-7777') também
era lido como DDD 55 em vez de número incompleto.

Passa a validar a forma do DDD e o 9º dígito do celular antes de
formatar, preservando o texto original quando a checagem falha —
mesmo comportamento já usado para números com contagem de dígitos
fora do padrão. Mantém DDD 55 (Rio Grande do Sul) funcionando
normalmente quando o DDD é digitado de fato.

Limitação residual conhecida e aceita: DDDs americanos que colidem
estruturalmente com um DDD brasileiro válido (ex. '2125551234', área
212 de Nova York, bate com DDD 21) continuam sendo formatados como
brasileiros — não há como distinguir os dois casos só pela forma dos
dígitos. Da mesma forma, um '55' sem o '+' explícito (ex.
'55988887777') é ambiguamente tratado como DDD 55 real, já que nada
no texto indica se é código de país ou área.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 02:48:55 -03:00
9fac784f6a feat: frontend audit — formulário de contato, a11y, SEO, performance e conteúdo (#13)
* chore: add husky pre-commit and pre-push hooks

* feat: frontend audit — formulário de contato, a11y, SEO, performance e conteúdo

* test: regenerate visual baselines from CI environment
2026-08-05 23:27:14 -03:00
3dc1f449ee feat: ship public site with SEO and visuals (#3)
* feat: ship public site with SEO and visuals

Publish CMS content on public routes with responsive media,
accessibility checks, and deterministic visual baselines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: make browser visual CI deterministic for media

Mount host public storage into FrankenPHP so seeded fixtures are served,
and replace PNG-as-JPG fixtures with real JPEGs so Chromium can render them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: serve public media on same-origin storage paths

Pest Browser hosts on 127.0.0.1:port while Storage::url used
http://localhost, so screenshots captured broken images. Use relative
/storage URLs for media and absolutize only OG tags via url().

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: refresh visual baselines from CI Ubuntu screenshots

Media now loads on same-origin /storage paths, so baselines must
capture the rendered fixtures. Use full-page snapshots from the CI
runner to keep Pest's exact snapshot match stable across environments.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 10:12:24 -03:00
236a7d3ea9 feat: Fase 0 — Fundação (setup-foundation) (#1)
* docs(openspec): propose setup-foundation change

Add OpenSpec change for SPEC Phase 0 — Foundation with proposal,
design, capability specs, and implementation tasks. Populate
openspec/config.yaml with project context and artifact rules.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: bootstrap Laravel 13 foundation (Fase 0)

Implement OpenSpec change setup-foundation: Laravel 13 + Filament 5
admin panel, Livewire 4 public site shell, PostgreSQL via Compose,
design tokens, role-based auth, quality gates (Pint/Larastan/Pest),
FrankenPHP Dockerfile, and GitHub Actions CI with five blocking jobs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): use valid APP_KEY for encryption in tests

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 22:05:25 -03:00