From dfeccf6c115e5bd7527d28b28330da6d3f2dd234 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Jan 2021 10:28:48 -0500 Subject: [PATCH] Move MailchimpBot to Mailchimp::Bot service (#12205) * Move MailchimpBot to Mailchimp::Bot service * Update specs * Put back old Mailchimp Bot --- app/models/user.rb | 2 +- app/services/mailchimp/bot.rb | 201 ++++++++++++++++++ .../moderator/manage_activity_and_roles.rb | 6 +- app/services/tag_moderators/add.rb | 2 +- .../tag_moderators/add_trusted_role.rb | 2 +- app/services/tag_moderators/remove.rb | 2 +- ...ubscribe_to_mailchimp_newsletter_worker.rb | 2 +- spec/services/mailchimp/bot_spec.rb | 141 ++++++++++++ ...ibe_to_mailchimp_newsletter_worker_spec.rb | 2 +- 9 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 app/services/mailchimp/bot.rb create mode 100644 spec/services/mailchimp/bot_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index ff9ee0010..e7e95039d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -500,7 +500,7 @@ class User < ApplicationRecord def unsubscribe_from_newsletters return if email.blank? - MailchimpBot.new(self).unsubscribe_all_newsletters + Mailchimp::Bot.new(self).unsubscribe_all_newsletters end def auditable? diff --git a/app/services/mailchimp/bot.rb b/app/services/mailchimp/bot.rb new file mode 100644 index 000000000..22c072104 --- /dev/null +++ b/app/services/mailchimp/bot.rb @@ -0,0 +1,201 @@ +module Mailchimp + class Bot + attr_reader :user, :saved_changes, :gibbon + + def initialize(user) + @user = user + @saved_changes = user.saved_changes + Gibbon::Request.api_key = SiteConfig.mailchimp_api_key + Gibbon::Request.timeout = 15 + @gibbon = Gibbon::Request.new + end + + def upsert + return true unless Rails.env.production? || Rails.env.test? + + manage_community_moderator_list + manage_tag_moderator_list + upsert_to_newsletter + end + + def upsert_to_newsletter + # attempt to update email if user changed email addresses + success = false + begin + gibbon.lists(SiteConfig.mailchimp_newsletter_id).members(target_md5_email).upsert( + body: { + email_address: user.email, + status: user.email_newsletter ? "subscribed" : "unsubscribed", + merge_fields: { + NAME: user.name.to_s, + USERNAME: user.username.to_s, + TWITTER: user.twitter_username.to_s, + GITHUB: user.github_username.to_s, + IMAGE_URL: user.profile_image_url.to_s, + ARTICLES: user.articles.size, + COMMENTS: user.comments.size, + ONBOARD_PK: user.onboarding_package_requested.to_s, + EXPERIENCE: user.experience_level || 666 + } + }, + ) + + success = true + rescue Gibbon::MailChimpError => e + # If user was previously subscribed, set their status to "pending" + return resubscribe_to_newsletter if previously_subcribed?(e) + + report_error(e) + end + success + end + + def resubscribe_to_newsletter + success = false + + begin + gibbon.lists(SiteConfig.mailchimp_newsletter_id).members(target_md5_email).upsert( + body: { status: "pending" }, + ) + success = true + rescue Gibbon::MailChimpError => e + report_error(e) + end + success + end + + def manage_community_moderator_list + return false unless user.has_role?(:trusted) + + success = false + status = user.email_community_mod_newsletter ? "subscribed" : "unsubscribed" + begin + gibbon.lists(SiteConfig.mailchimp_community_moderators_id).members(target_md5_email).upsert( + body: { + email_address: user.email, + status: status, + merge_fields: { + NAME: user.name.to_s, + USERNAME: user.username.to_s, + TWITTER: user.twitter_username.to_s, + GITHUB: user.github_username.to_s, + IMAGE_URL: user.profile_image_url.to_s + } + }, + ) + success = true + rescue Gibbon::MailChimpError => e + report_error(e) + end + success + end + + def manage_tag_moderator_list + return false unless user.tag_moderator? + + success = false + + tag_ids = user.roles.where(name: "tag_moderator").pluck(:resource_id) + tag_names = Tag.where(id: tag_ids).pluck(:name) + + status = user.email_tag_mod_newsletter ? "subscribed" : "unsubscribed" + + begin + gibbon.lists(SiteConfig.mailchimp_tag_moderators_id).members(target_md5_email).upsert( + body: { + email_address: user.email, + status: status, + merge_fields: { + NAME: user.name.to_s, + USERNAME: user.username.to_s, + TWITTER: user.twitter_username.to_s, + GITHUB: user.github_username.to_s, + IMAGE_URL: user.profile_image_url.to_s, + TAGS: tag_names.join(", ") + } + }, + ) + success = true + rescue Gibbon::MailChimpError => e + report_error(e) + end + success + end + + def unsub_sustaining_member + return unless user.tag_moderator? + + gibbon.lists(SiteConfig.mailchimp_tag_moderators_id).members(target_md5_email).update( + body: { + status: "unsubscribed" + }, + ) + end + + def unsub_community_mod + return unless user.has_role?(:trusted) + + gibbon.lists(SiteConfig.mailchimp_community_moderators_id).members(target_md5_email).update( + body: { + status: "unsubscribed" + }, + ) + end + + def unsub_tag_mod + return unless a_sustaining_member? + + gibbon.lists(SiteConfig.mailchimp_sustaining_members_id).members(target_md5_email).update( + body: { + status: "unsubscribed" + }, + ) + end + + def unsubscribe_all_newsletters + success = false + begin + gibbon.lists(SiteConfig.mailchimp_newsletter_id).members(target_md5_email).update( + body: { + status: "unsubscribed" + }, + ) + unsub_tag_mod + unsub_sustaining_member + unsub_community_mod + success = true + rescue Gibbon::MailChimpError => e + report_error(e) + end + success + end + + private + + def a_sustaining_member? + # Reasoning for including => saved_changes["monthly_dues"] + # Is that mailchimp should be updated if a user decides to + # unsubscribes + user.monthly_dues.positive? || saved_changes["monthly_dues"] + end + + def md5_email(email) + Digest::MD5.hexdigest(email.downcase) + end + + def report_error(exception) + Rails.logger.error(exception) + DatadogStatsClient.increment("mailchimp.errors", + tags: ["action:failed", "user_id:#{user.id}", "source:gibbon-gem"]) + end + + def target_md5_email + email = saved_changes["unconfirmed_email"] ? saved_changes["email"][0] : user.email + md5_email(email) + end + + def previously_subcribed?(error) + error.title.include?("Member In Compliance State") + end + end +end diff --git a/app/services/moderator/manage_activity_and_roles.rb b/app/services/moderator/manage_activity_and_roles.rb index 263dc5715..4dfc61692 100644 --- a/app/services/moderator/manage_activity_and_roles.rb +++ b/app/services/moderator/manage_activity_and_roles.rb @@ -35,14 +35,14 @@ module Moderator @user.remove_role :trusted @user.remove_role :tag_moderator @user.update(email_tag_mod_newsletter: false) - MailchimpBot.new(user).manage_tag_moderator_list + Mailchimp::Bot.new(user).manage_tag_moderator_list @user.update(email_community_mod_newsletter: false) - MailchimpBot.new(user).manage_community_moderator_list + Mailchimp::Bot.new(user).manage_community_moderator_list end def remove_tag_moderator_role @user.remove_role :tag_moderator - MailchimpBot.new(user).manage_tag_moderator_list + Mailchimp::Bot.new(user).manage_tag_moderator_list end def create_note(reason, content) diff --git a/app/services/tag_moderators/add.rb b/app/services/tag_moderators/add.rb index bbacf9ed8..c7263852c 100644 --- a/app/services/tag_moderators/add.rb +++ b/app/services/tag_moderators/add.rb @@ -54,7 +54,7 @@ module TagModerators Rails.cache.delete("user-#{user.id}/tag_moderators_list") return unless tag_mod_newsletter_enabled? - MailchimpBot.new(user).manage_tag_moderator_list + Mailchimp::Bot.new(user).manage_tag_moderator_list end def tag_mod_newsletter_enabled? diff --git a/app/services/tag_moderators/add_trusted_role.rb b/app/services/tag_moderators/add_trusted_role.rb index e64b2a39c..d2490e429 100644 --- a/app/services/tag_moderators/add_trusted_role.rb +++ b/app/services/tag_moderators/add_trusted_role.rb @@ -9,7 +9,7 @@ module TagModerators NotifyMailer.with(user: user).trusted_role_email.deliver_now return unless community_mod_newsletter_enabled? - MailchimpBot.new(user).manage_community_moderator_list + Mailchimp::Bot.new(user).manage_community_moderator_list end def self.community_mod_newsletter_enabled? diff --git a/app/services/tag_moderators/remove.rb b/app/services/tag_moderators/remove.rb index 77e63d1f1..b60d39d33 100644 --- a/app/services/tag_moderators/remove.rb +++ b/app/services/tag_moderators/remove.rb @@ -6,7 +6,7 @@ module TagModerators Rails.cache.delete("user-#{user.id}/tag_moderators_list") return unless tag_mod_newsletter_enabled? - MailchimpBot.new(user).manage_tag_moderator_list + Mailchimp::Bot.new(user).manage_tag_moderator_list end def self.tag_mod_newsletter_enabled? diff --git a/app/workers/users/subscribe_to_mailchimp_newsletter_worker.rb b/app/workers/users/subscribe_to_mailchimp_newsletter_worker.rb index 496226db0..b2b27266b 100644 --- a/app/workers/users/subscribe_to_mailchimp_newsletter_worker.rb +++ b/app/workers/users/subscribe_to_mailchimp_newsletter_worker.rb @@ -6,7 +6,7 @@ module Users def perform(user_id) user = User.find_by(id: user_id) - MailchimpBot.new(user).upsert if user + Mailchimp::Bot.new(user).upsert if user end end end diff --git a/spec/services/mailchimp/bot_spec.rb b/spec/services/mailchimp/bot_spec.rb new file mode 100644 index 000000000..c47c9c967 --- /dev/null +++ b/spec/services/mailchimp/bot_spec.rb @@ -0,0 +1,141 @@ +require "rails_helper" + +RSpec.describe Mailchimp::Bot, type: :service do + let(:user) { create(:user, :with_newsletters, :ignore_mailchimp_subscribe_callback) } + let(:article) { create(:article, user_id: user.id) } + let(:my_gibbon_client) { class_double Gibbon::Request } + let(:tag) do + create(:tag, + name: "tagname", + bg_color_hex: Faker::Color.hex_color, + text_color_hex: Faker::Color.hex_color, + supported: true) + end + + before do + allow(Gibbon::Request).to receive(:new) { my_gibbon_client } + allow(my_gibbon_client).to receive(:lists) { my_gibbon_client } + allow(my_gibbon_client).to receive(:tag_mods) { my_gibbon_client } + allow(my_gibbon_client).to receive(:community_mods) { my_gibbon_client } + allow(my_gibbon_client).to receive(:members) { my_gibbon_client } + allow(my_gibbon_client).to receive(:upsert).and_return(true) + end + + def matcher + { + body: { + email_address: user.email, + status: "subscribed", + merge_fields: { + NAME: user.name.to_s, + USERNAME: user.username.to_s, + TWITTER: user.twitter_username.to_s, + GITHUB: user.github_username.to_s, + IMAGE_URL: user.profile_image_url.to_s, + ARTICLES: user.articles.size, + COMMENTS: user.comments.size, + ONBOARD_PK: user.onboarding_package_requested.to_s, + EXPERIENCE: user.experience_level || 666 + } + } + } + end + + describe "#upsert" do + it "works" do + described_class.new(user).upsert + expect(my_gibbon_client).to have_received(:upsert) + end + end + + describe "#upsert_to_newsletter" do + it "sends proper information" do + described_class.new(user).upsert_to_newsletter + expect(my_gibbon_client).to have_received(:upsert).with(matcher) + end + + it "unsubscribes properly" do + user.update(email_newsletter: false) + described_class.new(user).upsert_to_newsletter + expect(my_gibbon_client).to have_received(:upsert) + .with(hash_including(body: hash_including(status: "unsubscribed"))) + end + + it "subscribes properly" do + user.update(email_newsletter: false) + user.update(email_newsletter: true) + described_class.new(user).upsert_to_newsletter + expect(my_gibbon_client).to have_received(:upsert) + .with(hash_including(body: hash_including(status: "subscribed"))) + end + + it "updates email properly" do + user.update(email: Faker::Internet.email) + user.confirm + described_class.new(user).upsert_to_newsletter + expect(my_gibbon_client).to have_received(:upsert) + .with(hash_including(body: hash_including(email_address: user.email))) + end + + it "tries to resubscribe the user if the user has previously been subscribed" do + user.update(email_newsletter: false) + mailchimp_bot = described_class.new(user) + mc_error = + Gibbon::MailChimpError.new("Error", status_code: 400, title: "Member In Compliance State") + allow(mailchimp_bot.gibbon).to receive(:upsert).and_raise(mc_error) + allow(mailchimp_bot).to receive(:resubscribe_to_newsletter) + + mailchimp_bot.upsert_to_newsletter + + expect(mailchimp_bot).to have_received(:resubscribe_to_newsletter) + end + end + + describe "manage community moderator list" do + it "returns false if user isn't a community moderator" do + expect(described_class.new(user).manage_community_moderator_list).to be(false) + end + + it "sends proper information" do + user.update(email_community_mod_newsletter: true) + user.add_role :trusted + described_class.new(user).manage_community_moderator_list + expect(my_gibbon_client).to have_received(:upsert) + .with(hash_including( + body: hash_including( + status: "subscribed", + ), + )) + end + end + + describe "manage tag moderator list" do + it "returns false if user isn't a tag moderator" do + expect(described_class.new(user).manage_community_moderator_list).to be(false) + end + + it "sends proper information" do + user.update(email_tag_mod_newsletter: true) + user.add_role(:tag_moderator, tag) + described_class.new(user).manage_tag_moderator_list + expect(my_gibbon_client).to have_received(:upsert) + .with(hash_including( + body: hash_including( + status: "subscribed", + ), + )) + end + end + + describe "#unsubscribe_all_newsletters" do + context "when called" do + before { allow(my_gibbon_client).to receive(:update).and_return(true) } + + it "unsubscribes the user from the weekly newsletter" do + described_class.new(user).unsubscribe_all_newsletters + expect(my_gibbon_client).to have_received(:update) + .with(hash_including(body: hash_including(status: "unsubscribed"))) + end + end + end +end diff --git a/spec/workers/users/subscribe_to_mailchimp_newsletter_worker_spec.rb b/spec/workers/users/subscribe_to_mailchimp_newsletter_worker_spec.rb index 59d609d17..6a1123807 100644 --- a/spec/workers/users/subscribe_to_mailchimp_newsletter_worker_spec.rb +++ b/spec/workers/users/subscribe_to_mailchimp_newsletter_worker_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Users::SubscribeToMailchimpNewsletterWorker, type: :worker do it "subscribes user to mailchimp newsletter" do mailchimp_bot = double - allow(MailchimpBot).to receive(:new).and_return(mailchimp_bot) + allow(Mailchimp::Bot).to receive(:new).and_return(mailchimp_bot) allow(mailchimp_bot).to receive(:upsert) worker.perform(user.id)