Corrigir ExpireWhenIneligible para expirar PRO com expires_at já no passado
TLDR:
Subscriptions::Pro::ExpireWhenIneligiblebusca a subscription PRO a expirar usando o scope.non_expired; seexpires_atjá passou, a busca retornanile o use case nunca marca a subscription comoinactivenem loga o evento — deixando-a presa emstatus: activepara sempre e bloqueando qualquer nova tentativa de assinatura PRO.
Contexto
Caso real em produção (usuária alessapena04@gmail.com, user_id 4891): a subscription PRO (id 767) está com status: active e expires_at: 2026-08-20 17:40:53 (no passado). A filiação no CITRG (CITRG::MembershipByEmail) está válida, aprovada e não suspensa (valid_until_timestamp: 2027-04-30, profile.status: "ok") — ou seja, ValidateEligibility calcularia pro_eligible: true hoje. Mesmo assim, user.active_pro? retorna false, porque user.subscription (has_one -> non_expired) ignora a linha 767 por causa do expires_at vencido e passa a apontar para a regular-terapeuta.
O problema é o finder em app/use_cases/subscriptions/pro/expire_when_ineligible.rb:21-24:
ruby
def pro_subscription
@pro_subscription ||= context.user.subscriptions.non_expired.find_by(
subscription_plan: SubscriptionPlan.pro,
status: [:pending, :active]
)
end
Se, no momento em que este use case roda (login, POST /api/v1/subscriptions/pros ou webhook do CITRG), o expires_at da PRO já estiver no passado, .non_expired exclui essa linha da busca. Consequência, em call:
```ruby def call return if context.pro_eligible return if context.citrg_unavailable
ActiveRecord::Base.transaction do pro_subscription.update!(status: :inactive, expires_at: Time.current) if pro_subscription.present? reactivate_regular_subscription end
return if pro_subscription.blank?
info(message: “Pro subscription expired due to ineligible CITRG membership”, metadata: {user_id: context.user.id}) end ```
pro_subscription.present?éfalse→ oupdate!(status: :inactive, ...)nunca roda → ostatusficaactivepara sempre, mesmo comexpires_atvencido.return if pro_subscription.blank?suprime o log informativo → nenhum rastro do evento (Subscription/SubscriptionPlannão usam a gemaudited, então também não há histórico emaudits).reactivate_regular_subscriptionroda de qualquer forma (sem guarda), reativando aregular-terapeuta— o app já reflete “usuário não é mais PRO” no dia a dia (active_pro?,user.plan), mas o registro da PRO em si fica inconsistente.
Esse estado inconsistente (status: active + expires_at no passado) é então enxergado por app/use_cases/subscriptions/pro/validate_existing_subscription.rb:5, que busca a PRO sem .non_expired:
ruby
subscription = context.user.subscriptions.where(subscription_plan:, status: [:pending, :active]).first
context.fail!(status: :unprocessable_entity, message: "Você já possui um plano PRO ativo") if subscription.present?
Essa linha ainda é encontrada (o .non_expired não é usado aqui) e bloqueia qualquer nova tentativa de assinar/renovar o PRO com "Você já possui um plano PRO ativo" — mesmo o usuário, na prática, não sendo mais PRO. O ciclo de repromoção descrito em R-003 (“Creation::CreateSubscription limpa expires_at ao reativar uma PRO existente”) depende de ExpireWhenIneligible ter conseguido marcar a PRO como inactive antes — o que não acontece nesse caso, então o usuário fica travado indefinidamente, mesmo depois de regularizar a filiação no CITRG.
Este spec é incremental sobre o trabalho já registrado em expire_pro_when_citrg_ineligible e fix_non_expired_scope_invert_where. O bug do invert_where no scope non_expired já foi corrigido em app/models/subscription.rb:37; este spec não mexe na definição do scope, apenas para de usá-lo neste finder específico.
Objetivos
ExpireWhenIneligiblemarca comoinactive(e loga) qualquer subscription PROpending/activedo usuário quandopro_eligibleéfalse, independentemente deexpires_atjá estar no passado- Contas presas nesse estado hoje (como a da usuária do caso real) se autocorrigem na próxima execução do use case (login,
POST /api/v1/subscriptions/prosou webhook do CITRG), sem depender de backfill manual
Fora de escopo
- Remediação manual/pontual da conta da usuária em produção (id 767) — resolve sozinha na próxima execução do use case, uma vez o fix aplicado
- Qualquer mudança em
ValidateExistingSubscription,Creation::CreateSubscriptionou na definição do scopeSubscription.non_expired - Auditoria/histórico de mudanças de
Subscription/SubscriptionPlan(ex.: adicionaraudited) — mencionado como lacuna observada, mas não faz parte deste fix - Constraint/validação no model
Subscriptionimpedindo o estadostatus: active+expires_atno passado — esse estado é usado de propósito porexpiry_other_active_plans(emPro::Creation::CreateSubscriptioneRegularTerapeuta::Creation::CreateSubscription) para marcar planos substituídos sem tocarstatus; mudar isso é uma decisão de modelo maior, fora do escopo deste bug
Mudanças
app/use_cases/subscriptions/pro/expire_when_ineligible.rb— remover.non_expireddo finderpro_subscription, deixando-o igual ao já usado emvalidate_existing_subscription.rb:5:ruby def pro_subscription @pro_subscription ||= context.user.subscriptions.find_by( subscription_plan: SubscriptionPlan.pro, status: [:pending, :active] ) endspec/use_cases/subscriptions/pro/expire_when_ineligible_spec.rb— novo teste cobrindo o cenário real: PRO comstatus: activeeexpires_atno passado +pro_eligible: false→ deve ficarinactivee o use case deve ter sucesso (log disparado)
Plano de implementação (TDD)
- test (RED): adicionar o cenário — usuário com
pro_subscriptionstatus: active,expires_at: 1.day.ago, chamado compro_eligible: false→ hoje falha porque a subscription permaneceactive - fix (GREEN): remover
.non_expireddo finderpro_subscription - Rodar a suíte completa de
expire_when_ineligible_spec.rbe dos specs relacionados (lista abaixo) para confirmar que nada regride
Como verificar
make test test=spec/use_cases/subscriptions/pro/expire_when_ineligible_spec.rbmake test test=spec/use_cases/subscriptions/pro/creation/create_flow_spec.rb spec/use_cases/subscriptions/pro/creation/create_subscription_spec.rb spec/use_cases/subscriptions/pro/validate_existing_subscription_spec.rb spec/requests/api/v1/subscriptions/pros_expire_when_ineligible_spec.rb spec/requests/citrg/membership_expirations_spec.rb— regressão nos fluxos que chamamExpireWhenIneligiblemake testverde- Console de produção (somente leitura, sem escrita): confirmar que a query sem
.non_expiredde fato encontra a subscription 767 da usuária do caso real:ruby user = User.find_by(email: "alessapena04@gmail.com") user.subscriptions.find_by(subscription_plan: SubscriptionPlan.pro, status: [:pending, :active]) # deve retornar a subscription 767, hoje ignorada pelo .non_expired
Documentação
- Atualizar R-003: a tabela de decisão já descreve o resultado esperado (“PRO expirado é reativado com
expires_atlimpo”) assumindo queExpireWhenIneligibleconsegue marcar a PRO comoinactivemesmo já expirada por data — hoje isso não era verdade; adicionar uma nota nas Restrições explicando que o finder depro_subscriptionnão filtra porexpires_at, exatamente para cobrir esse caso.