From 240d1ff2427634f0caa87c58ec864e8fbb27b73f Mon Sep 17 00:00:00 2001 From: Dana Janssen Date: Wed, 27 May 2026 11:17:01 -0600 Subject: [PATCH 1/3] Fix Mailchimp subscriber hash for set_list_member Co-Authored-By: Claude Opus 4.7 (1M context) --- Gemfile.lock | 3 +-- app/models/effective/mailchimp_api.rb | 15 ++++++++++++--- test/models/mailchimp_api_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 test/models/mailchimp_api_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 4315467..b52f080 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - effective_mailchimp (0.13.0) + effective_mailchimp (0.13.2) MailchimpMarketing effective_bootstrap effective_datatables (>= 4.0.0) @@ -391,7 +391,6 @@ DEPENDENCIES effective_test_bot haml-rails pry-byebug - psych sqlite3 wicked diff --git a/app/models/effective/mailchimp_api.rb b/app/models/effective/mailchimp_api.rb index 8476ba1..cf00f10 100644 --- a/app/models/effective/mailchimp_api.rb +++ b/app/models/effective/mailchimp_api.rb @@ -3,6 +3,7 @@ # https://mailchimp.com/developer/marketing/api/ require 'MailchimpMarketing' +require 'digest' module Effective class MailchimpApi @@ -92,7 +93,7 @@ def list_member(id, email) Rails.logger.info "[effective_mailchimp] Get List Member" if debug? begin - client.lists.get_list_member(id.try(:mailchimp_id) || id, email) + client.lists.get_list_member(id.try(:mailchimp_id) || id, subscriber_hash(email)) rescue MailchimpMarketing::ApiError => e {} end @@ -129,7 +130,7 @@ def list_member_add(member) # Actually add or update payload = list_member_payload(member) - client.lists.set_list_member(member.mailchimp_list.mailchimp_id, member.email, payload) + client.lists.set_list_member(member.mailchimp_list.mailchimp_id, subscriber_hash(member.user.email), payload) end def list_member_update(member) @@ -139,7 +140,8 @@ def list_member_update(member) return if sandbox_mode? payload = list_member_payload(member) - client.lists.update_list_member(member.mailchimp_list.mailchimp_id, member.email, payload) + hash = member.mailchimp_id.presence || subscriber_hash(member.email) + client.lists.update_list_member(member.mailchimp_list.mailchimp_id, hash, payload) end def list_member_payload(member) @@ -156,5 +158,12 @@ def list_member_payload(member) }.compact end + # Mailchimp identifies a list member by the MD5 hash of their lowercase email address + def subscriber_hash(email) + raise('expected an email') unless email.to_s.include?('@') + + Digest::MD5.hexdigest(email.to_s.downcase.strip) + end + end end diff --git a/test/models/mailchimp_api_test.rb b/test/models/mailchimp_api_test.rb new file mode 100644 index 0000000..3572bc8 --- /dev/null +++ b/test/models/mailchimp_api_test.rb @@ -0,0 +1,24 @@ +require 'test_helper' + +class MailchimpApiTest < ActiveSupport::TestCase + def api + @api ||= Effective::MailchimpApi.new(api_key: 'test-us1') + end + + test 'subscriber_hash is the md5 of the lowercase email' do + expected = Digest::MD5.hexdigest('brenda.barrera@quadreal.com') + + assert_equal expected, api.subscriber_hash('brenda.barrera@quadreal.com') + end + + test 'subscriber_hash lowercases and strips before hashing' do + expected = Digest::MD5.hexdigest('brenda.barrera@quadreal.com') + + assert_equal expected, api.subscriber_hash(' Brenda.Barrera@QuadReal.com ') + end + + test 'subscriber_hash raises without an email' do + assert_raises(RuntimeError) { api.subscriber_hash('not-an-email') } + end + +end From 9a44c826dae07fe6fadd0c3bc6d81e38ece53ab2 Mon Sep 17 00:00:00 2001 From: Dana Janssen Date: Wed, 27 May 2026 11:25:13 -0600 Subject: [PATCH 2/3] Send status_if_new when adding a Mailchimp list member set_list_member is a PUT upsert: status updates an existing contact while status_if_new is required to create a new one. The payload only sent status, so genuinely new subscribers failed validation. Mirror the desired status into status_if_new so new and existing members are both handled. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/models/effective/mailchimp_api.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/effective/mailchimp_api.rb b/app/models/effective/mailchimp_api.rb index cf00f10..fb3edbe 100644 --- a/app/models/effective/mailchimp_api.rb +++ b/app/models/effective/mailchimp_api.rb @@ -128,8 +128,8 @@ def list_member_add(member) Rails.logger.info "[effective_mailchimp] Add List Member" if debug? return if sandbox_mode? - # Actually add or update - payload = list_member_payload(member) + # Actually add or update. set_list_member applies status_if_new when the contact is new + payload = list_member_payload(member).merge(status_if_new: list_member_status(member)) client.lists.set_list_member(member.mailchimp_list.mailchimp_id, subscriber_hash(member.user.email), payload) end @@ -152,7 +152,7 @@ def list_member_payload(member) payload = { email_address: member.user.email, - status: (member.subscribed ? 'subscribed' : 'unsubscribed'), + status: list_member_status(member), merge_fields: merge_fields.transform_values { |value| value || '' }, interests: member.interests_hash.presence }.compact @@ -165,5 +165,9 @@ def subscriber_hash(email) Digest::MD5.hexdigest(email.to_s.downcase.strip) end + def list_member_status(member) + member.subscribed ? 'subscribed' : 'unsubscribed' + end + end end From ccae3959cc66d7a3da337573ab2a767391017888 Mon Sep 17 00:00:00 2001 From: Dana Janssen Date: Wed, 27 May 2026 11:26:05 -0600 Subject: [PATCH 3/3] Request full page of Mailchimp interest categories and interests The categories and interests calls omitted count, so Mailchimp's default page size of 10 applied and silently truncated the results. Lists with more than 10 interest categories, or a category with more than 10 groups, would drop the overflow and make those interests unsubscribable. Pass count: 1000 (the endpoint maximum) like the other index calls. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/models/effective/mailchimp_api.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/effective/mailchimp_api.rb b/app/models/effective/mailchimp_api.rb index fb3edbe..e128fec 100644 --- a/app/models/effective/mailchimp_api.rb +++ b/app/models/effective/mailchimp_api.rb @@ -76,14 +76,14 @@ def list(id) def categories(list_id) Rails.logger.info "[effective_mailchimp] Index Interest Categories" if debug? - response = client.lists.get_list_interest_categories(list_id.try(:mailchimp_id) || list_id) + response = client.lists.get_list_interest_categories(list_id.try(:mailchimp_id) || list_id, count: 1000) Array(response['categories']) - [nil, '', {}] end def interests(list_id, category_id) Rails.logger.info "[effective_mailchimp] Index Interest Category Interests" if debug? - response = client.lists.list_interest_category_interests(list_id, category_id) + response = client.lists.list_interest_category_interests(list_id, category_id, count: 1000) Array(response['interests']) - [nil, '', {}] end