Sincronización de Patreon v2 informa «N pledges, 0 users synced» — el correo de los miembros se lee del recurso incorrecto en la extracción

Aviso: Sí, usé Claude para ayudarme a solucionar este problema:

Ejecutando el núcleo de Discourse en el commit 5ee8e24 (2026-08-19). La sincronización con Patreon se completa sin errores, pero sincroniza cero usuarios:

Patreon sync complete: 63 pledges, 0 users synced

Las respuestas de la API son todas status=200, por lo que las solicitudes en sí están bien. Al rastrearlo en la consola de Rails, la solicitud de miembros que construye el plugin es correcta y sí solicita el correo electrónico en el recurso de miembro:

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

Al llamar directamente a ese punto de acceso con un token de creador correctamente limitado (con campaigns.members[email] concedido), el correo electrónico está presente en el objeto member, no en el objeto user dentro de included. Una búsqueda de un solo miembro lo confirma:

data.attributes.email        => "patron@example.com"   # presente
included[].attributes.email  => (ausente)               # los objetos user solo llevan full_name

Por lo tanto, en la lista de miembros de la campaña, member.attributes.email está poblado (46 de mis 63 miembros; el resto tiene el intercambio restringido o son seguidores gratuitos), mientras que las entradas de usuario en included no tienen ningún campo de correo electrónico en absoluto.

El problema está en Patreon::ApiVersion::V2.extract (lib/api_version/v2.rb). Construye el mapa users a partir de los objetos de usuario en 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 esos objetos de usuario nunca llevan el correo electrónico, el mapa sale vacío y cada miembro falla en el emparejamiento de correos, de ahí que sean 0 sincronizados.

Leer el correo electrónico desde la entrada del miembro en su lugar (usando como clave el id de usuario ya disponible en member.relationships.user.data.id, que es el mismo patron_id utilizado para las promesas) lo solucionó en mi instalación: 0 se convirtió en 46 users synced. El cambio que apliqué:

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

No estoy seguro de que eliminar por completo el bucle de included sea la decisión correcta para todos los casos: si algunas configuraciones sí reciben el correo electrónico en el objeto de usuario, es posible que deseen conservarlo como alternativa en lugar de reemplazarlo. Publico el comportamiento observado y el cambio mínimo que funcionó aquí, por si es útil o apunta a la solución correcta.