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
Usermas oUserProfileé rejeitado silenciosamente, deixando o usuário órfão e sem possibilidade de login. Corrigir a validação, falhar explicitamente, eliminar ohas_oneduplicado 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:
Therapists::SignInFlowautentica no CITRG corretamente (isCertified: true).Therapists::CreateLocalAccountchamaAccountCreator::Terapeuta.call, que dentro de uma transaction montaUser+UserProfile(viabuild_user_profile) +Subscription+FinancialSettinge faz@user.save!.- O CITRG IDP devolveu
document: "70848869000"— CPF inválido pelos dígitos verificadores. A validaçãodocument_numberdoUserProfilefalha. - O model
Usernão temvalidates_associated :user_profile, então o autosave dohas_onerejeita o profile silenciosamente:User.save!retornatrue, o profile não persiste, e a transaction é commitada com User + Subscription + FinancialSetting mas sem UserProfile. CreateLocalAccount#afterlêcontext.user.profile(nil) e devolveunprocessable_contentcomresult.message: nileresult.errors: nil— o frontend mostra a mensagem genérica “Não foi possível efetuar o login. Tente novamente.”- Em tentativas seguintes,
FindByIdpencontra oUserórfão e pula oCreateLocalAccount#call, mas o#afterainda 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::Terapeutafalhe explicitamente quando oUserProfileé inválido, retornando o erro real ao caller - Tolerar
document_numberinválido vindo do CITRG no momento do sign-in — persistir comonile exigir correção posterior no perfil - Eliminar a duplicação
has_one :profile/has_one :user_profileno modelUser - Fazer backfill: criar
UserProfilepara 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: :createpara 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 ahas_one :search_therapist, through: :profile(linha 88) — substituir todos os usos deuser.profileporuser.user_profile, euser.search_therapistporuser.user_profile.search_therapist(ou criarhas_one :search_therapist, through: :user_profile). - Atualizar o
delegate ..., to: :profile(linha 114) parato: :user_profile.
3. app/use_cases/therapists/create_local_account.rb
- Trocar
context.profile = context.user.profile(linha 16) porcontext.user.user_profile. - O
if context.profile.blank?continua como guarda, mas comvalidates_associatedpassa a ser rede de segurança em vez de o lugar onde falhas silenciosas aparecem. - Quando falhar, popular
context.messagecom algo útil (ex.: “Não foi possível concluir o cadastro local. Entre em contato com o suporte.”) em vez denil. 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
- Unitário — novo spec em
spec/use_cases/account_creator/terapeuta_spec.rb:- CPF válido vindo do CITRG → User + UserProfile criados,
document_numberpersistido - CPF inválido vindo do CITRG → User + UserProfile criados,
document_numbernil, sem exceção - UserProfile com slug duplicado (forçar colisão) →
ActiveRecord::RecordInvalidlevantado (graças aovalidates_associated)
- CPF válido vindo do CITRG → User + UserProfile criados,
-
Integração —
spec/use_cases/therapists/whitelistable_sign_in_flow_spec.rbcobrindo o caminho em que o mock deCITRG::Idpretorna CPF inválido e o flow precisa terminar com sucesso (não em:unprocessable_content) -
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 - 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
- Antes:
Documentação
- Criar um learning documentando o comportamento de autosave silencioso de
has_onesemvalidates_associated, e a regra: todahas_oneque faz parte da criação atômica de um agregado deve tervalidates_associatedno parent - Registrar o learning no índice