La sincronizzazione Patreon v2 riporta "N pledge, 0 utenti sincronizzati": e-mail dei membri lette dalla risorsa errata durante l'estrazione

Disclaimer: Sì, ho usato Claude per aiutarmi a risolvere questo problema:

Esecuzione del core di Discourse al commit 5ee8e24 (2026-08-19). La sincronizzazione con Patreon si completa senza errori, ma sincronizza zero utenti:

Patreon sync complete: 63 pledges, 0 users synced

Le risposte dell’API sono tutte status=200, quindi le richieste in sé sono corrette. Tracciando il problema nella console rails, la richiesta ai membri costruita dal plugin è corretta e richiede effettivamente l’email nella risorsa membro:

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

Chiamando direttamente quell’endpoint con un token creator correttamente scoped (campaigns.members[email] concesso), l’email è presente nell’oggetto member, non nell’oggetto user in included. Un fetch di un singolo membro lo conferma:

data.attributes.email        => "patron@example.com"   # presente
included[].attributes.email  => (assente)               # gli oggetti user contengono solo full_name

Quindi, nell’elenco dei membri della campagna, member.attributes.email è popolato (46 dei miei 63 membri; gli altri hanno condivisione limitata o sono follower gratuiti), mentre le voci utente in included non hanno affatto un campo email.

Il problema si trova in Patreon::ApiVersion::V2.extract (lib/api_version/v2.rb). Costruisce la mappa users dagli oggetti utente in 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

Poiché quegli oggetti utente non contengono mai l’email, la mappa risulta vuota e ogni membro fallisce il matching dell’email, da cui 0 sincronizzati.

Leggere l’email dalla voce membro invece (usando come chiave l’id utente già disponibile in member.relationships.user.data.id, che è lo stesso patron_id usato per le pledge) ha risolto il problema sulla mia installazione: da 0 a 46 users synced. La modifica che ho applicato:

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

Non sono certo che rimuovere completamente il loop included sia la scelta giusta in ogni caso — se in alcune configurazioni l’email viene ricevuta sull’oggetto utente, potresti volerlo mantenere come fallback anziché sostituirlo. Pubblico il comportamento osservato e la modifica minima che ha funzionato qui, nel caso possa essere utile o indichi la correzione giusta.