Fix: busca pública de terapeuta retorna um resultado por terapeuta

TLDR: Membership.search passa a colapsar as filiações por terapeuta, devolvendo uma linha por pessoa — a filiação ativa de maior valid_until. Com isso a “Busca por Terapeuta” deixa de exibir dois resultados para quem tem filiação vigente e renovação já aprovada, e o resultado único reflete a nova vigência.


Contexto

O caso relatado

Ao pesquisar uma terapeuta na funcionalidade “Busca por Terapeuta” do site do CITRG, aparecem dois resultados para a mesma pessoa. Os dois exibem o mesmo vencimento, em vez de representar a continuidade da vigência depois da renovação.

A terapeuta tem duas filiações consecutivas, ambas ativas hoje: a vigente e a renovação 2026–2027, que já foi aprovada.

Causa raiz

Membership.search (app/models/membership.rb) devolve uma linha por filiação, não por terapeuta:

ruby scope :search, ->(term) do active.joins(:user, :user_profile) .where("unaccent(users.name) ILIKE unaccent(?) OR memberships.register_number = ?", "#{term}%", term.to_i) end

O scope active filtra por valid_gte_today, que inclui toda filiação com vigência em andamento ou futura. Numa renovação antecipada as duas filiações passam no filtro, e Api::V2::TherapistController#index serializa as duas — duas linhas com o mesmo nome e o mesmo register_number.

O vencimento idêntico nos dois resultados é consequência disso, não um segundo defeito: TherapistsSerializer expõe apenas register e name, e é o register_number que vira o token do card (Base64 do registro). Como as duas linhas carregam o mesmo registro, clicar em qualquer uma chama Membership.active_current(register_number) e cai na mesma filiação — daí o mesmo vencimento nos dois.

Critério de escolha da filiação

active ordena por memberships.created_at DESC, e active_current faz search(register).first. “O registro criado por último” é o mesmo critério que já falhou no projeto quando passou a existir registro futuro — ver o aprendizado membership_newest_record_breaks_with_future_records. O critério correto para “até quando esta pessoa está filiada” é o maior valid_until, como já vale em R-001.


Objetivos

  • A busca pública devolve no máximo um resultado por terapeuta, mesmo com filiações consecutivas ativas.
  • A filiação escolhida para representar a terapeuta é a ativa de maior valid_until — logo, o card aberto a partir da busca reflete a nova vigência.
  • A paginação (current_page, total_count, total_pages) conta terapeutas, não filiações.

Fora de escopo

  • Alterar TherapistCardSerializer, TherapistsSerializer ou o contrato de resposta dos endpoints.
  • Alterar o scope active, usado por outros fluxos.
  • Corrigir dados em produção — o defeito é de consulta, não de dado.

Mudanças

app/models/membership.rb

search passa a colapsar por terapeuta. A seleção dos ids usa DISTINCT ON (user_id) ordenado por valid_until DESC, e o resultado volta como where(id: ...) para que Kaminari pagine e conte sobre a relação já colapsada:

ruby scope :search, ->(term) do where(id: active.joins(:user, :user_profile) .where("unaccent(users.name) ILIKE unaccent(?) OR memberships.register_number = ?", "#{term}%", term.to_i) .reorder("memberships.user_id, memberships.valid_until DESC, memberships.id DESC") .select("DISTINCT ON (memberships.user_id) memberships.id")) .order("memberships.created_at DESC") end

active_current continua sendo search(register_number).first e herda a escolha: com uma única linha por terapeuta, o .first passa a ser sempre a filiação de maior valid_until.

Nenhum outro ponto do código chama search ou active_current — apenas Api::V2::TherapistController.

Como verificar

Testes em test/controllers/api/v2/therapist_controller_test.rb:

  1. Duplicidade — terapeuta com filiação vigente + renovação aprovada, ambas ativas: busca pelo nome devolve items.length == 1 e total_count == 1.
  2. Vigência refletida — o card aberto pelo registro dessa terapeuta devolve end_at da renovação (a de maior valid_until).
  3. Terapeutas distintos — busca por termo que casa com duas pessoas diferentes continua devolvendo dois resultados.
  4. Regressão — os testes existentes de busca por nome, busca por registro, busca sem resultado e card seguem verdes.

bash make run.test path=test/controllers/api/v2/therapist_controller_test.rb make run.test path=test/models/membership_test.rb

Documentação

  • Criar regra em .project/docs/rules/membership/ — a busca pública devolve uma filiação por terapeuta, a de maior valid_until — e indexá-la em .project/docs/README.md e .project/docs/RULES.md.
  • Atualizar .project/docs/reference/api/public_endpoints.md com o comportamento de GET /api/v2/therapist/search/:term.