Foto na carteira digital auto-emitida e resultado único na busca pública

TLDR: Membership#automatic_approve! passa a anexar a foto do perfil na carteira — como manual_approve! já faz — e as filiações auto-aprovadas sem foto são corrigidas no dado; Membership.search passa a devolver uma filiação por terapeuta, a de valid_until mais distante, refletindo a continuidade da vigência após a renovação.

Contexto

Depois que a renovação automática entrou (PR #217), duas regressões apareceram na área pública do site do CITRG:

  1. Carteira digital sem foto. automatic_approve! nasceu sem o card_picture.attach que todos os caminhos de aprovação humana têm (manual_approve!, AdminMembershipsService#admin_approve_membership, o form do Admin). Como Api::V2::TherapistController#card resolve a filiação por Membership.active_current — que pega a mais nova — o card servido é sempre a renovação auto_issued, justamente a que não tem foto. O TherapistCardSerializer devolve photo_url: null.

  2. Duplicidade na busca pública. Membership.search devolve uma linha por filiação, não por terapeuta, e o scope active filtra só valid_until >= hoje — nunca valid_since. Como a renovação nasce com valid_since igual ao valid_until da vigente, as duas filiações são “ativas” hoje ao mesmo tempo. Antes da aprovação automática a renovação ficava not_issued com o perfil desaprovado e não passava em with_profile_approved_or_card_issued; com a carteira definitiva ela nasce auto_issued e o perfil auto-aprovado, então as duas passam. As duas compartilham o mesmo register_number, e o token do card é Base64(register_number) — por isso os dois resultados abrem o mesmo card e exibem o mesmo vencimento.

O apolo_membership já resolveu o mesmo problema de “duas filiações em continuidade” no R-001. A busca pública nunca recebeu esse tratamento.

Objetivos

  • Filiações auto-aprovadas a partir de agora nascem com card_picture anexado, igual às aprovadas manualmente.
  • As filiações já auto-aprovadas desde 12/08/2026, que estão sem card_picture, recebem o anexo por correção de dado — cerca de 100 registros.
  • A busca pública devolve um resultado por terapeuta, mesmo com filiações consecutivas.
  • O resultado devolvido é o da filiação de valid_until mais distante — a nova vigência após a renovação.
  • Membership.active_current deixa de escolher a filiação por created_at e passa a escolher pela vigência.

Fora de escopo

  • Fallback de leitura na carteira digital. TherapistCardSerializer continua exibindo só o card_picture da própria filiação: o passivo é corrigido no dado, não escondido na leitura. Filiação sem foto no perfil continua sem foto — é caso de pedir a foto à terapeuta.
  • Rake task para o backfill: são ~100 registros, corrigidos uma vez pelo console. Nada de arquivo one-off no repositório.
  • Composição de campos entre duas filiações no card, ao estilo do R-001 — o card continua vindo de uma filiação, agora a certa.
  • Alterar o register_number compartilhado entre filiações consecutivas, ou o formato do token do card.
  • MembershipApoloSerializer e Membership#public_serialize, que já tratam card_picture ausente com attached?.

Mudanças

A entrega é em dois passos: primeiro a foto da carteira (modelo + correção de dado), depois o resultado único na busca (scope :search).

app/models/membership.rb

  • automatic_approve! anexa a foto do perfil na carteira antes do update!, via um método privado attach_profile_picture_to_card extraído de manual_approve! — os dois caminhos passam a usá-lo.
  • scope :search passa a devolver uma linha por terapeuta: a filtragem atual vira subconsulta com DISTINCT ON (memberships.user_id) ordenada por valid_until DESC, id DESC, e o escopo externo (where(id: ...)) é ordenado por valid_until DESC, id DESC. A relação externa continua sendo uma ActiveRecord::Relation comum, então page/per/total_count do Kaminari seguem corretos — e total_count passa a contar terapeutas, não filiações.
  • active_current não muda de código: como search agora ordena por vigência, .first devolve a filiação de vigência mais distante em vez da criada por último.

Correção de dado (console de produção, uma vez)

Percorre as filiações auto_issued sem card_picture e anexa o blob da foto do perfil. Anexar reusa o blob — nenhum arquivo é enviado de novo. Filiação cujo usuário não tem foto no perfil é apenas listada.

Como verificar

Testes (make run.test path=<arquivo>):

  • test/models/membership_test.rb
    • automatic_approve! anexa a foto do perfil na carteira.
    • search devolve uma única filiação para o usuário com vigente + renovação aprovada, e é a de valid_until mais distante.
    • search continua devolvendo um resultado por terapeuta distinto quando dois usuários diferentes batem no termo.
    • active_current devolve a renovação, não a filiação vigente, quando as duas estão ativas.
  • test/controllers/api/v2/therapist_controller_test.rb
    • busca por nome e por register_number devolve items.length == 1 e pagination.total_count == 1 após a renovação auto-aprovada.
    • GET /api/v2/therapist/card/:token devolve a vigência da renovação.

Correção de dado: rodar a contagem antes e depois — Membership.where(card_status: :auto_issued).where.missing(:card_picture_attachment).count deve cair para o número de filiações cujo usuário não tem foto no perfil.

Ponta a ponta: buscar a terapeuta renovada no site do CITRG deve trazer um único resultado, e a carteira aberta a partir dele deve mostrar a foto e a vigência 2026–2027.

Documentação

  • Atualizar R-004 e R-009: automatic_approve! anexa a foto do perfil na carteira, igual a manual_approve!.
  • Criar regra nova em .project/docs/rules/membership/ — a busca pública devolve um resultado por terapeuta, com a vigência mais distante — e registrá-la em .project/docs/RULES.md.
  • Criar learning em .project/docs/learnings/ sobre o caminho automático que nasceu sem o efeito colateral do caminho manual.