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.