Caminho automático nasce sem os efeitos colaterais do caminho manual que ele espelha

O que aconteceu

Membership#automatic_approve! foi escrito em 12/08/2026 como o espelho de manual_approve!: grava card_status, admin_approved_at e admin_approved_by. Copiou o que estava no corpo do método e não o que o fluxo manual fazia em volta — entre outras coisas, o card_picture.attach da foto do perfil, que manual_approve!, AdminMembershipsService#admin_approve_membership e o form do Admin todos executam.

Nada quebrou no deploy. O efeito só apareceu na área pública do site do CITRG, semanas depois: a carteira digital das renovações auto-aprovadas saía com photo_url: null. Api::V2::TherapistController#card resolve a filiação por Membership.active_current, que pega a mais nova — ou seja, justamente a renovação auto_issued, a única sem foto. A filiação anterior, aprovada à mão, tinha a foto e nunca era servida.

Cerca de 100 filiações acumularam o defeito antes de alguém reparar.

Causa raiz

O espelhamento foi feito por semelhança de assinatura, não por equivalência de efeito. Os dois métodos têm o mesmo nome, os mesmos parâmetros e a mesma forma; um faz menos que o outro e nada no código diz isso.

Três coisas ajudaram a esconder:

  1. O efeito faltante é uma escrita satélite, não um campo do update!. Um atributo a menos no update! salta aos olhos na revisão lado a lado. Um attach que acontece três linhas antes, não.
  2. O caminho automático não tem testemunha. A aprovação manual tem um humano olhando a tela; a automática roda no webhook de pagamento e ninguém confere o resultado. Falha silenciosa em caminho sem operador só aparece pela reclamação do usuário final.
  3. Quem lê escolhe o registro mais novo. active_current pegava a filiação criada por último, que é exatamente a defeituosa. Se lesse a mais antiga, o bug ficaria invisível por mais tempo ainda — a leitura mascarava ou expunha o defeito por acidente, não por decisão.

Correção

O attach virou um método privado único, attach_profile_picture_to_card, chamado pelos dois ramos. Não há mais “a versão do manual” e “a versão do automático” — há um comportamento com dois chamadores.

As filiações já afetadas foram corrigidas no dado (anexando o blob da foto do perfil já existente), e não com fallback de leitura no serializer: a carteira continua exibindo o card_picture da própria filiação ou nada. Esconder o passivo na leitura teria deixado o defeito de escrita vivo.

Como evitar

  • Quando um método novo é descrito como “o espelho de X”, extrair o comportamento compartilhado para um lugar só no mesmo commit. Espelho mantido por cópia diverge; a única pergunta é quando.
  • Comparar os caminhos pelo efeito no banco, não pelo corpo do método: listar o que cada um escreve, incluindo anexos, callbacks e registros satélites. O que o manual faz em volta conta tanto quanto o que ele faz dentro.
  • Todo caminho automático merece um teste que afirme o estado final completo, não só o campo que motivou a mudança. Aqui bastava um assert membership.card_picture.attached?.
  • Ao introduzir um caminho automático paralelo a um manual, varrer quem lê o resultado. Se a leitura escolhe “o mais recente”, o registro produzido pelo caminho novo é o que vai aparecer — e qualquer defeito dele é imediatamente público.
  • Passivo gerado por escrita defeituosa se corrige no dado. Fallback no serializer conserta a tela e preserva o bug.

Relacionados