Sincronização do Patreon v2 relata "N compromissos, 0 usuários sincronizados" — e-mail do membro lido da fonte errada na extração

Aviso: Sim, usei o Claude para me ajudar a corrigir este problema:

Executando o núcleo do Discourse no commit 5ee8e24 (2026-08-19). A sincronização com o Patreon é concluída sem erros, mas sincroniza zero usuários:

Patreon sync complete: 63 pledges, 0 users synced

As respostas da API são todas status=200, então as próprias requisições estão funcionando corretamente. Ao rastrear no console do Rails, a requisição de membros que o plugin constrói está correta e solicita o e-mail no recurso de membro:

fields[member]=full_name,last_charge_date,last_charge_status,currently_entitled_amount_cents,patron_status,email

Ao chamar esse endpoint diretamente com um token de criador adequadamente escopado (campaigns.members[email] concedido), o e-mail está presente no objeto member, e não no objeto user em included. Uma busca de membro único confirma isso:

data.attributes.email        => "patron@example.com"   # presente
included[].attributes.email  => (ausente)              # os objetos user contêm apenas full_name

Portanto, na lista de membros da campanha, member.attributes.email está preenchido (46 dos meus 63 membros; o restante tem compartilhamento restrito ou são seguidores gratuitos), enquanto as entradas de usuário em included não possuem nenhum campo de e-mail.

O problema está em Patreon::ApiVersion::V2.extract (lib/api_version/v2.rb). Ele constrói o mapa users a partir dos objetos de usuário em included:

ruby

(member_data["included"] || []).each do |entry|
  if entry["type"] == "user" && entry["attributes"]["email"].present?
    users[entry["id"]] = entry["attributes"]["email"].downcase
  end
end

Como esses objetos de usuário nunca carregam o e-mail, o mapa sai vazio e todos os membros falham na correspondência de e-mail, resultando em 0 sincronizados.

Ler o e-mail da entrada do membro em vez disso (chaveado pelo id do usuário já disponível em member.relationships.user.data.id, que é o mesmo patron_id usado para as doações) corrigiu o problema na minha instalação: 0 se tornou 46 users synced. A alteração que apliquei:

diff

+            users[patron_id] = attrs["email"].downcase if attrs["email"].present?
             pledges[patron_id] = attrs["currently_entitled_amount_cents"]
             declines[patron_id] = attrs["last_charge_date"] if attrs["last_charge_status"] == "Declined"
           end
-
-          (member_data["included"] || []).each do |entry|
-            if entry["type"] == "user" && entry["attributes"]["email"].present?
-              users[entry["id"]] = entry["attributes"]["email"].downcase
-            end
-          end

Não tenho certeza se remover completamente o loop de included é a decisão certa para todos os casos — se algumas configurações recebem o e-mail no objeto de usuário, talvez seja melhor mantê-lo como um fallback em vez de substituí-lo. Estou postando o comportamento observado e a alteração mínima que funcionou aqui, caso seja útil ou aponte para a correção adequada.