Escolher “o registro mais recente” quebra quando passa a existir registro futuro
O que aconteceu
O endpoint GET /api/v1/apolo_membership selecionava a filiação paga de maior valid_until para representar a situação do membro. Em uma renovação antecipada o usuário fica com duas filiações pagas simultâneas: a vigente (aprovada) e uma futura, que nasce sem aprovação de admin. Como a futura sempre tem o maior valid_until, ela era sempre a escolhida — e, não estando aprovada, apolo_access_permitted? retornava false. O Apolo gravava apolo_access_enabled = false e bloqueava os cursos comuns de quem tinha renovado adiantado e estava em dia. 192 membros foram afetados.
Cursos is_cbtrg continuavam liberados porque dependem de outra chave (cbtrg_expires_at, que recebia a data mais distante) — o que produziu a assimetria confusa que aparecia no ticket.
Causa raiz
“O registro mais recente” só é um bom proxy para “o registro que representa a entidade agora” enquanto todos os registros estão no passado. Quando o modelo passa a admitir registro futuro (renovação antecipada), o critério de ordenação deixa de responder à pergunta que estava sendo feita.
O agravante é que havia duas perguntas diferentes sendo respondidas pelo mesmo registro: “esta pessoa está em conformidade agora?” (situação) e “até quando ela pagou?” (data final). A primeira exige a filiação vigente; a segunda exige a mais distante. Um único registro não podia responder às duas.
Correção
A resposta passou a ser composta: os campos de situação vêm da filiação vigente (valid_until >= hoje) mais recente que passa no gate de elegibilidade, e os campos de data vêm da filiação paga de maior valid_until. Um fallback para a seleção anterior garante que nenhum usuário perca acesso.
Como evitar
- Ao selecionar um registro para representar uma entidade com histórico versionado, separar as perguntas antes de escrever a query: campos que descrevem situação atual precisam de critério de vigência + elegibilidade; campos que descrevem fato acumulado (até quando pagou, quanto pagou) precisam do extremo do histórico. Se as duas naturezas convivem no mesmo payload, compor é mais correto do que escolher.
- Uma mudança de seleção só é provadamente aditiva se tiver fallback para o comportamento anterior. Sem o fallback, o caso “nenhuma candidata passa no gate” viraria
404— e, aqui, um404fazia otrg-club-apirebaixar a assinatura PRO de quem estava pago, exatamente o bug que a correção anterior tinha resolvido. - Antes de escrever o código, simular a mudança sobre a base inteira e classificar cada usuário por resultado (inalterado / mudou-mesmo-resultado / corrigido / regressão). Foi essa simulação que reprovou a primeira variante da correção (“a mais antiga ainda válida”, sem checar o gate), que causava 4 regressões de acesso invisíveis no raciocínio.