Patreon v2の同期レポートで「N件のpledge、同期済みユーザー0件」と表示される — extract内で誤ったリソースからメンバーのメールアドレスが読み取られている

免責事項: はい、この問題の修正にClaudeの助けを借りました。

Discourse core はコミット 5ee8e24 (2026-08-19) で実行しています。Patreon の同期はエラーなく完了しますが、同期されるユーザーはゼロです:

Patreon sync complete: 63 pledges, 0 users synced

API のレスポンスはすべて status=200 なので、リクエスト自体には問題ありません。Rails コンソールでトレースすると、プラグインが構築しているメンバーリクエストは正しく、member リソースに対して email の取得を要求しています:

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

適切にスコープされたクリエイタートークン (campaigns.members[email] が付与されている) を直接使用してそのエンドポイントを呼び出すと、email は included 内の user オブジェクトではなく、member オブジェクト上に存在します。単一のメンバー取得でも確認できます:

data.attributes.email        => "patron@example.com"   # 存在
included[].attributes.email  => (なし)               # user オブジェクトは full_name のみ

つまり、キャンペーンメンバーリストでは、member.attributes.email が設定されています(私の63人のメンバーのうち46人。残りは共有が制限されているか、無料フォロワーです)。一方、included の user エントリには email フィールドが全くありません。

問題は Patreon::ApiVersion::V2.extract (lib/api_version/v2.rb) にあります。これは included の user オブジェクトから users マップを構築しています:

ruby

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

これらの user オブジェクトには email が含まれないため、マップは空になり、すべてのメンバーが email マッチングに失敗し、結果として同期数が 0 になっています。

代わりに member エントリから email を読み取ること(pledges で使用されているのと同じ patron_id である member.relationships.user.data.id をキーとして利用可能)で、私の環境では修正されました: 046 users synced に変わりました。適用した変更は以下の通りです:

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

included ループを完全に削除することが、すべてのケースにおいて正しい判断であるとは確信できません。一部の環境では user オブジェクトに email が返される可能性があるため、置き換えではなくフォールバックとして残すことを検討してもよいでしょう。参考になるか、あるいは適切な修正のヒントになるかと思い、観察された挙動と、ここで機能した最小限の変更を投稿します。