Bloquear terapeuta logado de acessar o funil de checkout

TLDR: um terapeuta logado (regular ou pro) consegue hoje completar o checkout e agendar sessão com outro terapeuta; a correção estende o routesGuard para bloquear qualquer terapeuta logado nas 3 páginas do funil de checkout, mantendo cliente e visitante anônimo funcionando como hoje.

Status: proposed Created: 2026-08-07 Owner: @diegoFranciscoo


Context

A regra de negócio do produto é que o fluxo de agendamento/checkout é exclusivo para clientes (isCustomer) e visitantes anônimos (que se cadastram como cliente durante o próprio checkout). Terapeutas não deveriam conseguir agendar sessão com outro terapeuta por esse fluxo.

Essa regra já existe e funciona corretamente na página de perfil público /terapeuta/[slug]: o backend (TherapistSectionsManager, documentado em .project/docs/specs/20260806105144_professional_profile_contact_card.md) retorna a seção personal_contact (só o card de informações) em vez de scheduling (Agenda) quando quem está visualizando o perfil é um terapeuta logado. O frontend consome isso em PublicProfile.tsx via showSchedulling, então um terapeuta logado normalmente nunca vê o widget de Agenda.

O problema é que essa checagem nunca foi replicada nas páginas do funil de checkout. Reprodução do bug:

  1. Visitante anônimo escolhe uma sessão com um terapeuta e é redirecionado para /checkout/{slug}/info?sessions=... sem estar logado.
  2. Em vez de logar inline nessa mesma aba (fluxo normal do checkout), o usuário abre uma nova aba e loga com uma conta de terapeuta já existente (regular ou pro).
  3. Ele cola a mesma URL de checkout na aba autenticada.
  4. A página carrega normalmente — nenhuma das páginas do funil valida o tipo da sessão logada — e ele consegue completar o pagamento e agendar a sessão com o terapeuta-alvo.

Nenhuma das 3 páginas do funil (pages/checkout/[slug]/info.tsx, pages/app/checkout/[slug]/cartao.tsx, pages/app/checkout/[slug]/pix.tsx) valida isso hoje. info.tsx fica fora do matcher de middleware atual (["/app/:path*", "/cadastro/:path*"]), por isso o routesGuard nunca roda para ela.

Objetivos

  • Bloquear qualquer terapeuta logado (regular ou pro) de acessar /checkout/{slug}/info, /app/checkout/{slug}/cartao e /app/checkout/{slug}/pix, redirecionando para /buscar-terapeuta.
  • Manter o acesso de visitante anônimo a /checkout/{slug}/info funcionando exatamente como hoje (sem exigir login).
  • Manter o acesso de cliente logado (isCustomer) funcionando exatamente como hoje nas 3 páginas.
  • Consolidar a checagem num único validator do routesGuard, seguindo o padrão já usado por minhaCarteiraValidator, meusClientesValidator, etc., em vez de duplicar a lógica em cada getServerSideProps.

Non-goals

  • Nenhuma mudança de backend/API.
  • Nenhuma diferenciação entre terapeuta regular e pro — a regra bloqueia terapeuta de qualquer tier, sem exceção.
  • Nenhuma mudança na página /terapeuta/[slug] ou no PublicProfile.tsx — já funcionam corretamente.
  • Nenhuma mudança de UX para o cliente ou visitante anônimo.

Changes

  1. middleware.ts — adicionar "/checkout/:path*" ao array matcher, que hoje é ["/app/:path*", "/cadastro/:path*"].

  2. src/infra/routesGuard/callbacks.ts (authGuard) — adicionar uma exceção pontual, no mesmo padrão da exceção já existente para /cadastro/certificado-invalido, que retorna true para o pathname que casa com /^\/checkout\/[^/]+\/info$/, permitindo que o visitante anônimo continue acessando essa página sem login. Fora dessa exceção, mantém Boolean(token).

  3. Novo validator src/infra/routesGuard/validators/checkoutValidator/ — implementa GuardValidator (mesmo formato do minhaCarteiraValidator: checkoutValidator.tsx com a classe + index.ts reexportando).
    • shouldValidate(req): testa req.nextUrl.pathname contra /^\/(app\/)?checkout\/[^/]+\/(info|cartao|pix)$/.
    • validate(req): extrai session do req.nextauth.token (mesmo padrão do minhaCarteiraValidator). Se session existir e !isCustomer({ user: session } as TUserSession) (helper já existente em @/infra/permissionsManager), define this.redirectPath = new URL("/buscar-terapeuta", req.url) e retorna false. Caso contrário, retorna true.
    • Visitante anônimo (sem token) nunca chega a rodar o validator, pois middleware.ts já retorna cedo quando !req.nextauth.token.
  4. src/infra/routesGuard/validators/index.ts — registrar new CheckoutValidator() em getValidators(), junto dos demais validators existentes.

How to verify

  • Teste automatizado do novo checkoutValidator (seguindo o padrão de restrictedRoutesForRolesValidator.test.ts, já que é o único validator existente com teste): cobrir os 3 pathnames (info, cartao, pix) × 3 tipos de sessão (sem sessão/anônimo — não deve nem chegar ao validate via middleware, mas o validate() isolado deve retornar true se session for undefined; cliente; terapeuta regular; terapeuta pro).
  • Manual: como visitante anônimo, escolher uma sessão e chegar em /checkout/{slug}/info sem estar logado — deve carregar normalmente.
  • Manual: reproduzir o cenário do bug (abrir nova aba, logar como terapeuta regular, colar a URL de checkout) — deve redirecionar para /buscar-terapeuta.
  • Manual: repetir o mesmo teste logado como terapeuta pro — deve redirecionar para /buscar-terapeuta também.
  • Manual: como cliente logado, completar o fluxo de checkout normalmente até cartao/pix — deve funcionar sem bloqueio.

Documentation

Nenhuma mudança de documentação de regra de negócio necessária — a regra em si (“agendamento é só para cliente/anônimo”) já está documentada em .project/docs/specs/20260806105144_professional_profile_contact_card.md; esta spec apenas fecha uma lacuna de enforcement que não seguia essa regra já existente.