Sempre exibir o terapeuta na busca, independente de disponibilidade

TLDR: Remove a exigência de ao menos uma disponibilidade cadastrada para que um terapeuta Pro apareça na busca — o filtro fechava a busca por dois caminhos e escondia terapeutas que passavam em todos os demais critérios.

Contexto

A terapeuta Rita Valente Fialho Canudo (SearchTherapist id 10421, plan_id 1 = Pro, availabilities_count: 0) não aparecia em GET /api/v1/therapists?name=canudo&page=1 apesar de passar em todos os demais critérios (pro_bono_allowed: true, price: 150.0, nome batendo com canudo).

A causa raiz era o filtro base de UserProfiles::SearchTherapistsQuery#eligible_therapists, que exigia, para terapeutas Pro, ao menos uma disponibilidade cadastrada (availabilities_count >= 1). Havia ainda uma regra equivalente e complementar em SearchTherapists::UpdateByProfile#valid?, que destruía o registro de um Pro assim que ele ficava sem Availability — fechando a busca por um segundo caminho.

A nova regra de negócio é sempre mostrar o terapeuta (Pro ou Regular) na busca, independente de ter disponibilidade cadastrada. Fora essa regra, todos os demais filtros (plano ativo, onboarding aprovado, perfil publicado, pro bono, preço para cliente, etc.) permanecem.

O contador availabilities_count continua sendo alimentado porque ainda é exibido no dashboard Avo (app/views/avo/cards/_search_therapist_count.html.erb:14-15).

Onde UpdateByProfile é chamado

SearchTherapists::UpdateByProfile.call tem um único caller: SearchTherapists::RefreshByProfile#call (app/use_cases/search_therapists/refresh_by_profile.rb:10). Portanto todo impacto da mudança em UpdateByProfile#valid? passa pelos gatilhos de RefreshByProfile:

Gatilhos síncronos de RefreshByProfile.call

  1. Job SearchTherapists::RefreshByProfileJob — app/jobs/search_therapists/refresh_by_profile_job.rb:4. Ponto final de todo fluxo assíncrono.
  2. Cron a cada minuto — TherapistAvailableTimes::UpdateLastProfilesChangedJob → itera perfis mexidos recentemente e chama RefreshByProfile.call(profile.user_id) em app/use_cases/therapist_available_times/update_last_profiles_changed.rb:17.
  3. Cron a cada hora (minuto 0) — TherapistAvailableTimes::RefreshJob → percorre TODOS os perfis com search_therapist em app/jobs/therapist_available_times/refresh_job.rb:5.
  4. SearchTherapists::Refresh — app/use_cases/search_therapists/refresh.rb:5. Itera UserProfile.published. Sem caller no repo; uso manual via console para reindexação em massa.

Callbacks que enfileiram RefreshByProfileJob via EnqueueRefreshByProfile (com dedup e delay de 2 s)

Model Hook Arquivo:linha Condição
UserProfile after_commit user_profile.rb:241 qualquer mudança
UserProfileCertificate after_commit user_profile_certificate.rb:29 qualquer mudança
Availability after_commit availability.rb:159-161 qualquer mudança
UserAddress after_commit user_address.rb:64 qualquer mudança
FinancialSetting after_commit financial_setting.rb:58 qualquer mudança
Subscription before_update subscription.rb:47-51 (pro? OR terapeuta?) AND active?
Meeting after_commit meeting.rb:187 qualquer mudança
MeetingParticipant after_commit meeting_participant.rb:82 professional?
Rate after_commit rate.rb:38 qualquer mudança

Callback que enfileira direto (sem passar por EnqueueRefreshByProfile): User#before_update em app/models/user.rb:187-193, quando name_changed?, agenda SearchTherapists::RefreshByProfileJob com delay de 2 s.

Efeito concreto da remoção da checagem user.pro? && !user.availabilities.exists?:

  • Nenhum Pro é destruído da tabela por perder Availability. Qualquer edição nos models acima passa a recriar registros de Pros hoje ausentes.
  • Via cron horário, em até 1 h a tabela converge para o novo universo sem ação manual.
  • Para reindexar imediatamente após o deploy: SearchTherapists::Refresh.call no console.
  • No caso da Rita, o registro já existia; a remoção do filtro da query bastava para ela aparecer. A mudança no valid? é uma salvaguarda contra remoções futuras.

Objetivos

  • Exibir terapeutas Pro e Regular na busca independentemente de terem disponibilidade cadastrada.
  • Impedir que um Pro seja removido da tabela search_therapists por ficar sem Availability.
  • Preservar todos os demais filtros de elegibilidade da busca.

Fora de escopo

  • Coluna search_therapists.availabilities_count — permanece no schema.
  • Atualização do contador em UpdateByProfile (linha 22) — alimenta o dashboard Avo.
  • Callbacks de EnqueueRefreshByProfile nos 11 modelos (Availability, Subscription, etc.) — continuam essenciais para manter os demais campos (price, score, plan_id, name) sincronizados.
  • Filtro by_user (pro bono para terapeutas, plan_id = PRO + price > 0 para clientes).
  • Demais checagens em UpdateByProfile#valid?.
  • Specs de request em spec/requests/api/v1/search_therapists_index_as_* — o shared context when_setup_searchable_therapist.rb já cria availability para todos os perfis, então continuam verdes sem alteração.
  • Ordenação/score da Rita (score: 0 a coloca no fim do ranking) — outra discussão, em SearchTherapists::CalculateScore.

Mudanças

1. app/queries/user_profiles/search_therapists_query.rb

Simplificar initialize e remover o método eligible_therapists:

ruby def initialize @collection = SearchTherapist.distinct end

Remover o body do initialize que calculava pro_plan_id e o método eligible_therapists. A constante DEFAULT_ORDER_DIRECTION permanece — continua em uso no default_order. A query passa a partir do universo completo de SearchTherapist; os demais escopos (by_user, by_name, by_price, by_date, default_order) continuam intactos e agem em cima disso.

2. app/use_cases/search_therapists/update_by_profile.rb

Remover a linha:

ruby return false if user.pro? && !user.availabilities.exists?

As demais checagens do valid? (onboarding, published, is_therapist, subscription presente, slug whitelisted, não expirada, ativa) permanecem. A atribuição de availabilities_count permanece.

3. spec/queries/user_profiles/search_therapists_query_spec.rb

  • Contexto “when PRO therapist has no availabilities”: inverter a expectativa para to include(search_therapist) e atualizar as descrições.
  • Contexto “when both PRO and regular therapists exist”: trocar expect(collection).not_to include(pro_without_availability) por expect(collection).to include(pro_without_availability).
  • Demais contextos permanecem inalterados.

4. spec/use_cases/search_therapists/update_by_profile_spec.rb

  • Contexto “when user is PRO without availabilities”: inverter a expectativa para be true.
  • Contexto “when PRO user loses availabilities”: o registro não é mais destruído — expectativa final expect(SearchTherapist.exists?(profile_id: profile.id)).to be true.

Como verificar

  1. Testes unitários afetados: bash make test test=spec/queries/user_profiles/search_therapists_query_spec.rb make test test=spec/use_cases/search_therapists/update_by_profile_spec.rb

  2. Testes de request (sanity check, não devem quebrar): bash make test test=spec/requests/api/v1/search_therapists_index_as_anonymous_spec.rb make test test=spec/requests/api/v1/search_therapists_index_as_client_spec.rb make test test=spec/requests/api/v1/search_therapists_index_as_therapist_spec.rb

  3. Verificação manual no caso da Rita (make console) — antes deve retornar 0, depois 1: ruby UserProfiles::SearchTherapistsQuery.new .by_name("canudo") .instance_variable_get(:@collection) .pluck(:id, :name)

  4. Smoke test end-to-end — autenticar como cliente e bater em GET /api/v1/therapists?name=canudo&page=1, confirmando que a Rita aparece.

  5. Regressão pro bono — autenticar como terapeuta (com e sem a flag cost_reduction_phase_one) e confirmar que os filtros pro_bono_allowed e quota continuam aplicados sobre o universo expandido.

Documentação

Mudança de comportamento observável: terapeutas Pro recém-aprovados passam a aparecer na busca antes de cadastrar qualquer horário. Isso pode impactar a métrica de “terapeutas ativos exibidos” — vale comunicar ao produto antes de subir.