Sign-in de terapeuta cria User órfão sem UserProfile

TLDR: O primeiro sign-in de um terapeuta cujo CPF no CITRG IDP é inválido cria o User mas o UserProfile é rejeitado silenciosamente, deixando o usuário órfão e sem possibilidade de login. Corrigir a validação, falhar explicitamente, eliminar o has_one duplicado e fazer backfill dos órfãos existentes.

Contexto

Edivania de Souza da Silva (Asana 1215375963780361) não conseguia logar no TRG Club. A investigação mostrou:

  1. Therapists::SignInFlow autentica no CITRG corretamente (isCertified: true).
  2. Therapists::CreateLocalAccount chama AccountCreator::Terapeuta.call, que dentro de uma transaction monta User + UserProfile (via build_user_profile) + Subscription + FinancialSetting e faz @user.save!.
  3. O CITRG IDP devolveu document: "70848869000" — CPF inválido pelos dígitos verificadores. A validação document_number do UserProfile falha.
  4. O model User não tem validates_associated :user_profile, então o autosave do has_one rejeita o profile silenciosamente: User.save! retorna true, o profile não persiste, e a transaction é commitada com User + Subscription + FinancialSetting mas sem UserProfile.
  5. CreateLocalAccount#after lê context.user.profile (nil) e devolve unprocessable_content com result.message: nil e result.errors: nil — o frontend mostra a mensagem genérica “Não foi possível efetuar o login. Tente novamente.”
  6. Em tentativas seguintes, FindByIdp encontra o User órfão e pula o CreateLocalAccount#call, mas o #after ainda lê profile (nil) e falha de novo. A conta fica eternamente quebrada.

Há também uma duplicação confusa no model User (app/models/user.rb:86-87):

ruby has_one :user_profile, dependent: :destroy has_one :profile, dependent: :destroy, class_name: "UserProfile"

Duas has_one apontando para a mesma tabela user_profiles com a mesma FK. O CreateLocalAccount#after lê user.profile enquanto o AccountCreator::Terapeuta chama build_user_profile — é coincidência funcionar quando funciona; a duplicação é uma cilada esperando acontecer.

Volume potencial: cadastros antigos no Apolo (formações de 2022 e anteriores) aceitavam CPF inválido. Outros terapeutas com CPF mal cadastrado no CITRG podem estar no mesmo estado órfão:

ruby User.left_joins(:user_profile) .where(user_profiles: {id: nil}) .where.not(citrg_idp_user_id: nil) .count

Objetivos

  • Garantir que AccountCreator::Terapeuta falhe explicitamente quando o UserProfile é inválido, retornando o erro real ao caller
  • Tolerar document_number inválido vindo do CITRG no momento do sign-in — persistir como nil e exigir correção posterior no perfil
  • Eliminar a duplicação has_one :profile / has_one :user_profile no model User
  • Fazer backfill: criar UserProfile para os usuários órfãos já existentes em produção

Fora de escopo

  • Bloquear o sign-in por CPF inválido — o sign-in não é o lugar de fazer essa validação

Mudanças

1. app/use_cases/account_creator/terapeuta.rb

Sanitizar document_number antes de atribuir: se o CPF não passar no CPF.valid? (gem cpf_cnpj), salvar como nil. O profile fica com account_status: :draft, que já exige o usuário completar os dados depois.

ruby def normalized_document doc = @params[:document].to_s.gsub(/\D/, "") return nil if doc.blank? CPF.valid?(doc) ? doc : nil end

2. app/models/user.rb

  • Adicionar validates_associated :user_profile, on: :create para falhar explicitamente em qualquer caminho que crie User+Profile com profile inválido. Defensivo contra regressões.
  • Remover has_one :profile, dependent: :destroy, class_name: "UserProfile" (linha 87) e a has_one :search_therapist, through: :profile (linha 88) — substituir todos os usos de user.profile por user.user_profile, e user.search_therapist por user.user_profile.search_therapist (ou criar has_one :search_therapist, through: :user_profile).
  • Atualizar o delegate ..., to: :profile (linha 114) para to: :user_profile.

3. app/use_cases/therapists/create_local_account.rb

  • Trocar context.profile = context.user.profile (linha 16) por context.user.user_profile.
  • O if context.profile.blank? continua como guarda, mas com validates_associated passa a ser rede de segurança em vez de o lugar onde falhas silenciosas aparecem.
  • Quando falhar, popular context.message com algo útil (ex.: “Não foi possível concluir o cadastro local. Entre em contato com o suporte.”) em vez de nil. O frontend já mostra mensagem genérica, mas pelo menos os logs ficam acionáveis.

4. Backfill em rake task: lib/tasks/backfill_orphan_therapist_profiles.rake

Re-buscar os dados via CITRG::Idp.new não é possível (exige senha). A alternativa viável é construir um UserProfile mínimo, que o usuário completa no primeiro login bem-sucedido:

```ruby namespace :therapists do task backfill_orphan_profiles: :environment do orphans = User.left_joins(:user_profile) .where(user_profiles: {id: nil}) .where.not(citrg_idp_user_id: nil)

orphans.find_each do |user|
  UserProfile.create!(
    user: user,
    slug: BuildSlug.call(user.name || "user-#{user.id}"),
    document_type: :cpf,
    account_status: :draft
  )
  puts "✓ user #{user.id}"
rescue => e
  puts "✗ user #{user.id}: #{e.message}"
end   end end ```

Como verificar

  1. Unitário — novo spec em spec/use_cases/account_creator/terapeuta_spec.rb:
    • CPF válido vindo do CITRG → User + UserProfile criados, document_number persistido
    • CPF inválido vindo do CITRG → User + UserProfile criados, document_number nil, sem exceção
    • UserProfile com slug duplicado (forçar colisão) → ActiveRecord::RecordInvalid levantado (graças ao validates_associated)
  2. Integração — spec/use_cases/therapists/whitelistable_sign_in_flow_spec.rb cobrindo o caminho em que o mock de CITRG::Idp retorna CPF inválido e o flow precisa terminar com sucesso (não em :unprocessable_content)

  3. Manual em staging — forjar uma resposta do CITRG IDP com CPF inválido para um e-mail novo e tentar logar: a conta deve ser criada normalmente, com profile em document_number: nil

  4. Backfill em staging primeiro, depois produção:
    • Antes: User.left_joins(:user_profile).where(user_profiles: {id: nil}).where.not(citrg_idp_user_id: nil).count
    • Rodar rake therapists:backfill_orphan_profiles
    • Depois: a contagem deve ser 0

Documentação

  • Criar um learning documentando o comportamento de autosave silencioso de has_one sem validates_associated, e a regra: toda has_one que faz parte da criação atômica de um agregado deve ter validates_associated no parent
  • Registrar o learning no índice