Fix: busca pública de terapeuta retorna um resultado por terapeuta
TLDR:
Membership.searchpassa a colapsar as filiações por terapeuta, devolvendo uma linha por pessoa — a filiação ativa de maiorvalid_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,TherapistsSerializerou 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:
- Duplicidade — terapeuta com filiação vigente + renovação aprovada, ambas ativas: busca pelo nome devolve
items.length == 1etotal_count == 1. - Vigência refletida — o card aberto pelo registro dessa terapeuta devolve
end_atda renovação (a de maiorvalid_until). - Terapeutas distintos — busca por termo que casa com duas pessoas diferentes continua devolvendo dois resultados.
- 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 maiorvalid_until— e indexá-la em.project/docs/README.mde.project/docs/RULES.md. - Atualizar
.project/docs/reference/api/public_endpoints.mdcom o comportamento deGET /api/v2/therapist/search/:term.