├── .rspec ├── .phantom-version ├── app ├── assets │ ├── javascripts │ │ └── spree │ │ │ ├── backend │ │ │ └── spree_chimpy.js │ │ │ └── frontend │ │ │ └── spree_chimpy.js │ └── stylesheets │ │ └── spree │ │ ├── backend │ │ └── spree_chimpy.css │ │ └── frontend │ │ └── spree_chimpy.css ├── models │ └── spree │ │ ├── chimpy │ │ ├── order_source.rb │ │ ├── subscriber.rb │ │ └── configuration.rb │ │ ├── user_decorator.rb │ │ └── order_decorator.rb ├── views │ └── spree │ │ ├── shared │ │ ├── _user_subscription.html.erb │ │ └── _guest_subscription.html.erb │ │ └── admin │ │ └── users │ │ ├── _subscription.html.erb │ │ └── _subscription_form.html.erb ├── overrides │ ├── user_form.rb │ └── admin_user.rb └── controllers │ └── spree │ └── chimpy │ └── subscribers_controller.rb ├── config ├── initializers │ └── spree_chimpy.rb ├── routes.rb └── locales │ ├── en.yml │ ├── de.yml │ ├── sv.yml │ └── nl.yml ├── spec ├── support │ ├── chimpy.rb │ ├── factory_girl.rb │ ├── spree.rb │ └── database_cleaner.rb ├── factories │ ├── user_factory.rb │ └── subscriber_factory.rb ├── controllers │ ├── spree │ │ └── chimpy │ │ │ └── subscribers_controller_spec.rb │ └── controller_filters_spec.rb ├── models │ ├── subscriber_spec.rb │ ├── user_spec.rb │ └── order_spec.rb ├── lib │ ├── orders_interface_spec.rb │ ├── chimpy_spec.rb │ ├── products_interface_spec.rb │ ├── order_upserter_spec.rb │ ├── customers_interface_spec.rb │ ├── subscription_spec.rb │ └── list_interface_spec.rb ├── requests │ └── subscribers_spec.rb ├── features │ └── spree │ │ ├── subscription_spec.rb │ │ └── admin │ │ └── subscription_spec.rb └── spec_helper.rb ├── .gitignore ├── bin └── rails ├── db └── migrate │ ├── 20130422122600_add_subscribed_to_spree_users.rb │ ├── 201304241925_create_order_sources.rb │ └── 20130509170447_create_subscriber.rb ├── Gemfile ├── Rakefile ├── lib ├── spree │ └── chimpy │ │ ├── workers │ │ ├── resque.rb │ │ ├── delayed_job.rb │ │ └── sidekiq.rb │ │ ├── interface │ │ ├── orders.rb │ │ ├── customer_upserter.rb │ │ ├── order_upserter.rb │ │ ├── products.rb │ │ └── list.rb │ │ ├── controller_filters.rb │ │ ├── subscription.rb │ │ └── engine.rb ├── generators │ ├── spree_chimpy │ │ └── install │ │ │ └── install_generator.rb │ └── templates │ │ └── spree_chimpy.rb ├── spree_chimpy.rb └── tasks │ └── spree_chimpy.rake ├── wercker.yml ├── Guardfile ├── spree_chimpy.gemspec ├── LICENSE.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.phantom-version: -------------------------------------------------------------------------------- 1 | 1.9.7 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/spree_chimpy.js: -------------------------------------------------------------------------------- 1 | //= require spree/backend 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/spree_chimpy.js: -------------------------------------------------------------------------------- 1 | //= require spree/frontend 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/spree_chimpy.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/backend 3 | */ 4 | -------------------------------------------------------------------------------- /config/initializers/spree_chimpy.rb: -------------------------------------------------------------------------------- 1 | Spree::PermittedAttributes.user_attributes << :subscribed 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/spree_chimpy.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/frontend 3 | */ 4 | -------------------------------------------------------------------------------- /spec/support/chimpy.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | 3 | config.before do 4 | Spree::Chimpy.reset 5 | end 6 | end -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Spree::Core::Engine.add_routes do 2 | namespace :chimpy, path: "" do 3 | resource :subscribers, only: [:create] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/user_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user_with_subscribe_option, parent: :user do 3 | subscribed false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | require 'factory_girl' 2 | 3 | FactoryGirl.find_definitions 4 | 5 | RSpec.configure do |config| 6 | config.include FactoryGirl::Syntax::Methods 7 | end -------------------------------------------------------------------------------- /app/models/spree/chimpy/order_source.rb: -------------------------------------------------------------------------------- 1 | class Spree::Chimpy::OrderSource < ActiveRecord::Base 2 | self.table_name = :spree_chimpy_order_sources 3 | belongs_to :order, class_name: 'Spree::Order' 4 | end 5 | -------------------------------------------------------------------------------- /spec/factories/subscriber_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :subscriber, class: Spree::Chimpy::Subscriber do 3 | sequence(:email) { |n| "foo#{n}@email.com" } 4 | subscribed true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \#* 2 | *~ 3 | .#* 4 | .DS_Store 5 | .idea 6 | .project 7 | .sass-cache 8 | coverage 9 | Gemfile.lock 10 | tmp 11 | nbproject 12 | pkg 13 | *.swp 14 | spec/dummy 15 | .bundle 16 | .node-version 17 | .ruby-version 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 4 | ENGINE_PATH = File.expand_path('../../lib/spree_chimpy/engine', __FILE__) 5 | 6 | require 'rails/all' 7 | require 'rails/engine/commands' 8 | -------------------------------------------------------------------------------- /app/views/spree/shared/_user_subscription.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= f.check_box :subscribed %> 4 | <%= f.label :subscribed, Spree.t(:subscribed_label, scope: :chimpy) %> 5 |

6 |
7 | -------------------------------------------------------------------------------- /app/views/spree/admin/users/_subscription.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= Spree.t(:subscribed, scope: :chimpy) %> 3 | 4 | <%= @user.subscribed ? Spree.t(:say_yes) : Spree.t(:say_no) %> 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/views/spree/admin/users/_subscription_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= f.check_box :subscribed %> 4 | <%= f.label :subscribed, Spree.t(:admin_subscribed_label, scope: :chimpy) %> 5 |

6 |
7 | -------------------------------------------------------------------------------- /db/migrate/20130422122600_add_subscribed_to_spree_users.rb: -------------------------------------------------------------------------------- 1 | class AddSubscribedToSpreeUsers < ActiveRecord::Migration 2 | def change 3 | change_table Spree.user_class.table_name.to_sym do |t| 4 | t.boolean :subscribed 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'spree', github: 'spree/spree', branch: '2-3-stable' 4 | gem 'spree_auth_devise', github: "spree/spree_auth_devise", branch: '2-3-stable' 5 | gem 'libnotify' 6 | gem 'fuubar' 7 | gem 'byebug' 8 | gem 'pry-byebug' 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /db/migrate/201304241925_create_order_sources.rb: -------------------------------------------------------------------------------- 1 | class CreateOrderSources < ActiveRecord::Migration 2 | def change 3 | create_table :spree_chimpy_order_sources do |t| 4 | t.references :order 5 | t.string :campaign_id, :email_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130509170447_create_subscriber.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscriber < ActiveRecord::Migration 2 | def change 3 | create_table :spree_chimpy_subscribers do |t| 4 | t.string :email, null: false 5 | t.boolean :subscribed, default: true 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/overrides/user_form.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new(:virtual_path => "spree/shared/_user_form", 2 | :name => "user_form_subscription", 3 | :insert_after => "[data-hook=signup_below_password_fields]", 4 | :partial => "spree/shared/user_subscription") 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | require 'spree/testing_support/common_rake' 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | task :default => [:spec] 10 | 11 | desc 'Generates a dummy app for testing' 12 | task :test_app do 13 | ENV['LIB_NAME'] = 'spree_chimpy' 14 | Rake::Task['common:test_app'].invoke 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/spree.rb: -------------------------------------------------------------------------------- 1 | require 'spree/testing_support/factories' 2 | require 'spree/testing_support/controller_requests' 3 | require 'spree/testing_support/authorization_helpers' 4 | require 'spree/testing_support/url_helpers' 5 | require 'spree/testing_support/capybara_ext' 6 | 7 | RSpec.configure do |config| 8 | config.include Spree::TestingSupport::UrlHelpers 9 | config.include Spree::TestingSupport::ControllerRequests 10 | end -------------------------------------------------------------------------------- /app/views/spree/shared/_guest_subscription.html.erb: -------------------------------------------------------------------------------- 1 | <% @subscriber ||= Spree::Chimpy::Subscriber.new %> 2 | <%= form_for @subscriber do |f| %> 3 |
4 |

<%= Spree.t(:subscribe_title, scope: :chimpy) %>

5 | <%= f.label :email, Spree.t(:label, scope: [:chimpy, :email]) %> 6 | <%= f.email_field :email, placeholder: Spree.t(:placeholder, scope: [:chimpy, :email]) %> 7 | <%= f.submit %> 8 |
9 | <% end %> 10 | -------------------------------------------------------------------------------- /spec/controllers/spree/chimpy/subscribers_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::SubscribersController do 4 | 5 | let(:spree_chimpy_subscriber) { create(:subscriber) } 6 | let(:valid_attributes) { attributes_for(:subscriber) } 7 | 8 | context '#create' do 9 | it 'raise error when empty hash found' do 10 | expect { spree_post :create, chimpy_subscriber: {} }.to raise_error 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/spree/chimpy/workers/resque.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Workers 3 | class Resque 4 | delegate :log, to: Spree::Chimpy 5 | 6 | QUEUE = :default 7 | @queue = QUEUE 8 | 9 | def self.perform(payload) 10 | Spree::Chimpy.perform(payload.with_indifferent_access) 11 | rescue Excon::Errors::Timeout, Excon::Errors::SocketError 12 | log "Mailchimp connection timeout reached, closing" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | spree: 4 | chimpy: 5 | subscribed_label: Sign me up to the newsletter 6 | admin_subscribed_label: Subscribed to newsletter 7 | subscribe_title: Subscribe to our newsletter 8 | subscribed: Subscribed 9 | uncategorized: Uncategorized 10 | email: 11 | label: Email 12 | placeholder: Enter your email address 13 | subscriber: 14 | success: Thanks! you've been successfully added 15 | failure: There was a problem processing your sign-up 16 | -------------------------------------------------------------------------------- /lib/spree/chimpy/workers/delayed_job.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Workers 3 | class DelayedJob 4 | delegate :log, to: Spree::Chimpy 5 | 6 | def initialize(payload) 7 | @payload = payload 8 | end 9 | 10 | def perform 11 | Spree::Chimpy.perform(@payload) 12 | rescue Excon::Errors::Timeout, Excon::Errors::SocketError 13 | log "Mailchimp connection timeout reached, closing" 14 | end 15 | 16 | def max_attempts 17 | return 3 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/rvm 2 | build: 3 | steps: 4 | - bundle-install 5 | - script: 6 | name: echo ruby information 7 | code: | 8 | echo "ruby version $(ruby --version) running" 9 | echo "from location $(which ruby)" 10 | echo -p "gem list: $(gem list)" 11 | 12 | - script: 13 | name: build app 14 | code: bundle exec rake test_app 15 | 16 | - script: 17 | name: rspec 18 | code: bundle exec rspec 19 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | spree: 4 | chimpy: 5 | subscribed_label: Newsletter abonnieren 6 | admin_subscribed_label: Für Newsletter angemeldet 7 | subscribe_title: Für unseren Newsletter anmelden 8 | subscribed: Für Newsletter angemeldet 9 | uncategorized: Unkategorisiert 10 | email: 11 | label: Email 12 | placeholder: E-Mail Adresse eingeben 13 | subscriber: 14 | success: Danke! Du wurdest erfolgreich hinzugefügt 15 | failure: Es gabe ein Problem bei der Newsletter Anmeldung 16 | -------------------------------------------------------------------------------- /app/models/spree/user_decorator.rb: -------------------------------------------------------------------------------- 1 | if Spree.user_class 2 | Spree.user_class.class_eval do 3 | 4 | after_create :subscribe 5 | after_destroy :unsubscribe 6 | after_initialize :assign_subscription_default 7 | 8 | delegate :subscribe, :resubscribe, :unsubscribe, to: :subscription 9 | 10 | private 11 | def subscription 12 | Spree::Chimpy::Subscription.new(self) 13 | end 14 | 15 | def assign_subscription_default 16 | self.subscribed ||= Spree::Chimpy::Config.subscribed_by_default if new_record? 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/spree/order_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Order.class_eval do 2 | has_one :source, class_name: 'Spree::Chimpy::OrderSource' 3 | 4 | state_machine do 5 | after_transition :to => :complete, :do => :notify_mail_chimp 6 | end 7 | 8 | around_save :handle_cancelation 9 | 10 | def notify_mail_chimp 11 | Spree::Chimpy.enqueue(:order, self) if completed? && Spree::Chimpy.configured? 12 | end 13 | 14 | private 15 | def handle_cancelation 16 | canceled = state_changed? && canceled? 17 | yield 18 | notify_mail_chimp if canceled 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/locales/sv.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sv: 3 | spree: 4 | chimpy: 5 | subscribed_label: Registrera mig till nyhetsbrevet 6 | admin_subscribed_label: Prenumererat på nyhetsbrev 7 | subscribe_title: Prenumerera på vårt nyhetsbrev 8 | subscribed: Prenumererat 9 | uncategorized: Okategoriserad 10 | email: 11 | label: Epost 12 | placeholder: Ange din e-postadress 13 | subscriber: 14 | success: Tack! du har lagts till i vår prenumerations lista 15 | failure: Det uppstod ett problem med att registrera din sign-up 16 | -------------------------------------------------------------------------------- /app/overrides/admin_user.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new(:virtual_path => "spree/admin/users/_form", 2 | :name => "admin_user_form_subscription", 3 | :insert_after => "[data-hook=admin_user_form_fields]", 4 | :partial => "spree/admin/users/subscription_form") 5 | 6 | Deface::Override.new(:virtual_path => "spree/admin/users/show", 7 | :name => "admin_user_show_subscription", 8 | :insert_after => "table tr:last", 9 | :partial => "spree/admin/users/subscription") 10 | -------------------------------------------------------------------------------- /lib/spree/chimpy/workers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Workers 3 | class Sidekiq 4 | delegate :log, to: Spree::Chimpy 5 | if defined?(::Sidekiq) 6 | include ::Sidekiq::Worker 7 | sidekiq_options queue: :mailchimp, retry: 3, 8 | backtrace: true 9 | end 10 | 11 | def perform(payload) 12 | Spree::Chimpy.perform(payload.with_indifferent_access) 13 | rescue Excon::Errors::Timeout, Excon::Errors::SocketError 14 | log "Mailchimp connection timeout reached, closing" 15 | end 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/spree/chimpy/subscriber.rb: -------------------------------------------------------------------------------- 1 | class Spree::Chimpy::Subscriber < ActiveRecord::Base 2 | self.table_name = "spree_chimpy_subscribers" 3 | 4 | EMAIL_REGEX = /\A([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i 5 | 6 | validates :email, presence: true 7 | validates_format_of :email, with: EMAIL_REGEX, allow_blank: false, if: :email_changed? 8 | 9 | after_create :subscribe 10 | around_update :resubscribe 11 | after_destroy :unsubscribe 12 | 13 | delegate :subscribe, :resubscribe, :unsubscribe, to: :subscription 14 | 15 | private 16 | def subscription 17 | Spree::Chimpy::Subscription.new(self) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | require 'database_cleaner' 2 | 3 | RSpec.configure do |config| 4 | 5 | config.before(:suite) do 6 | DatabaseCleaner.strategy = :transaction 7 | DatabaseCleaner.clean_with(:truncation) 8 | end 9 | 10 | config.before(:each) do |example| 11 | if example.metadata[:js] 12 | DatabaseCleaner.strategy = :truncation 13 | else 14 | DatabaseCleaner.start 15 | end 16 | end 17 | 18 | config.after(:each) do |example| 19 | DatabaseCleaner.clean 20 | 21 | if example.metadata[:js] 22 | DatabaseCleaner.strategy = :transaction 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/locales/nl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nl: 3 | spree: 4 | chimpy: 5 | subscribed_label: "Schrijf mij in voor de nieuwsbrief" 6 | admin_subscribed_label: "Ingeschreven voor de nieuwsbrief" 7 | subscribed: "Ingeschreven" 8 | subscribe_title: "Schrijf je in voor onze nieuwsbrief" 9 | uncategorized: Ongecatagoriseerd 10 | email: 11 | label: "Blijf op de hoogte van onze aanbiedingen en ontvang exclusieve korting" 12 | placeholder: "Voer hier je email adres in" 13 | subscriber: 14 | success: "Bedankt voor je inschrijving" 15 | failure: "Er ging helaas iets mis bij het verwerken van je inschrijving" 16 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', cmd: 'bundle exec rspec', all_on_start: true, all_after_pass: false do 2 | watch('spec/spec_helper.rb') { 'spec' } 3 | watch('config/routes.rb') { 'spec/controllers' } 4 | watch(%r{^spec/(.+)_spec\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 5 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 6 | watch(%r{^app/(.*)(\.erb)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 7 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 8 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" } 9 | end -------------------------------------------------------------------------------- /spec/models/subscriber_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Subscriber do 4 | context "without email" do 5 | it 'is not valid without an email' do 6 | record = described_class.new(email: nil) 7 | record.valid? 8 | expect(record.errors[:email].size).to eq(1) 9 | end 10 | 11 | it 'can be valid' do 12 | expect(described_class.new(email: 'test@example.com')).to be_valid 13 | end 14 | end 15 | 16 | context 'with wrong email' do 17 | it 'is invalid when bad domain' do 18 | expect(described_class.new(email: 'test@example')).to_not be_valid 19 | end 20 | 21 | it 'is invalid when missing @domain' do 22 | expect(described_class.new(email: 'test')).to_not be_valid 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spree/chimpy/interface/orders.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Interface 3 | class Orders 4 | delegate :log, :store_api_call, to: Spree::Chimpy 5 | 6 | def initialize 7 | end 8 | 9 | def add(order) 10 | OrderUpserter.new(order).upsert 11 | end 12 | 13 | def remove(order) 14 | log "Attempting to remove order #{order.number}" 15 | 16 | begin 17 | store_api_call.orders(order.number).delete 18 | rescue Gibbon::MailChimpError => e 19 | log "error removing #{order.number} | #{e.raw_body}" 20 | end 21 | end 22 | 23 | def sync(order) 24 | add(order) 25 | rescue Gibbon::MailChimpError => e 26 | log "invalid ecomm order error [#{e.raw_body}]" 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/spree/chimpy/subscribers_controller.rb: -------------------------------------------------------------------------------- 1 | class Spree::Chimpy::SubscribersController < ApplicationController 2 | respond_to :html, :json 3 | 4 | def create 5 | @subscriber = Spree::Chimpy::Subscriber.where(email: subscriber_params[:email]).first_or_initialize 6 | @subscriber.email = subscriber_params[:email] 7 | @subscriber.subscribed = subscriber_params[:subscribed] 8 | if @subscriber.save 9 | flash[:notice] = Spree.t(:success, scope: [:chimpy, :subscriber]) 10 | else 11 | flash[:error] = Spree.t(:failure, scope: [:chimpy, :subscriber]) 12 | end 13 | 14 | referer = request.referer || root_url # Referer is optional in request. 15 | respond_with @subscriber, location: referer 16 | end 17 | 18 | private 19 | 20 | def subscriber_params 21 | params.require(:chimpy_subscriber).permit(:email, :subscribed) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/spree/chimpy/configuration.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | class Configuration < Spree::Preferences::Configuration 3 | preference :store_id, :string, default: 'spree' 4 | preference :subscribed_by_default, :boolean, default: false 5 | preference :subscribe_to_list, :boolean, default: false 6 | preference :key, :string 7 | preference :list_name, :string, default: 'Members' 8 | preference :list_id, :string, default: nil 9 | preference :customer_segment_name, :string, default: 'Customers' 10 | preference :merge_vars, :hash, default: { 'EMAIL' => :email } 11 | preference :api_options, :hash, default: { timeout: 60 } 12 | preference :double_opt_in, :boolean, default: false 13 | preference :send_welcome_email, :boolean, default: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/orders_interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Interface::Orders do 4 | let(:interface) { described_class.new } 5 | 6 | let(:store_api) { double(:store_api) } 7 | let(:order_api) { double(:order_api) } 8 | 9 | let(:order) { create(:order) } 10 | 11 | before(:each) do 12 | allow(Spree::Chimpy).to receive(:store_api_call) { store_api } 13 | end 14 | 15 | context "adding an order" do 16 | it "calls the order upserter" do 17 | 18 | expect_any_instance_of(Spree::Chimpy::Interface::OrderUpserter).to receive(:upsert) 19 | interface.add(order) 20 | end 21 | end 22 | 23 | it "removes an order" do 24 | expect(store_api).to receive(:orders) 25 | .with(order.number) 26 | .and_return(order_api) 27 | 28 | expect(order_api).to receive(:delete) 29 | .and_return(true) 30 | 31 | expect(interface.remove(order)).to be_truthy 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/generators/spree_chimpy/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module SpreeChimpy 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | class_option :auto_run_migrations, type: :boolean, default: false 5 | 6 | source_root File.expand_path('../../../templates/', __FILE__) 7 | 8 | def copy_initializer_file 9 | copy_file 'spree_chimpy.rb', "config/initializers/spree_chimpy.rb" 10 | end 11 | 12 | def add_migrations 13 | run 'bundle exec rake railties:install:migrations FROM=spree_chimpy' 14 | end 15 | 16 | def run_migrations 17 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]') 18 | if run_migrations 19 | run 'bundle exec rake db:migrate' 20 | else 21 | puts 'Skipping rake db:migrate, don\'t forget to run it!' 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spree/chimpy/controller_filters.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module ControllerFilters 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | before_action :set_mailchimp_params 7 | before_action :find_mail_chimp_params, if: :mailchimp_params? 8 | include ::Spree::Core::ControllerHelpers::Order 9 | end 10 | 11 | private 12 | 13 | attr_reader :mc_eid, :mc_cid 14 | 15 | def set_mailchimp_params 16 | @mc_eid = params[:mc_eid] || session[:mc_eid] 17 | @mc_cid = params[:mc_cid] || session[:mc_cid] 18 | end 19 | 20 | def mailchimp_params? 21 | (!mc_eid.nil? || !mc_cid.nil?) && 22 | (!session[:order_id].nil? || !params[:record_mc_details].nil?) 23 | end 24 | 25 | def find_mail_chimp_params 26 | attributes = { campaign_id: mc_cid, email_id: mc_eid } 27 | if current_order(create_order_if_necessary: true).source 28 | current_order.source.update_attributes(attributes) 29 | else 30 | current_order.create_source(attributes) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/controllers/controller_filters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ::Spree::StoreController do 4 | controller(::Spree::StoreController) do 5 | def index 6 | head :ok 7 | end 8 | end 9 | 10 | let(:user) { create(:user) } 11 | subject { controller } 12 | 13 | before do 14 | allow(subject).to receive(:try_spree_current_user).and_return(user) 15 | subject.session[:order_id] = 'R1919' 16 | end 17 | 18 | it 'sets the attributes for order if eid/cid is set in session' do 19 | subject.session[:mc_eid] = '1234' 20 | subject.session[:mc_cid] = 'abcd' 21 | expect(subject).to receive(:find_mail_chimp_params) 22 | 23 | get :index 24 | end 25 | 26 | it 'sets the attributes for the order if eid/cid is set in the params' do 27 | expect(subject).to receive(:find_mail_chimp_params) 28 | 29 | get :index, mc_eid: '1234', mc_cid: 'abcd' 30 | end 31 | 32 | it 'does not call find mail chimp params method if no eid/cid' do 33 | expect(subject).to_not receive(:find_mail_chimp_params) 34 | 35 | get :index 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::User do 4 | context "syncing with mail chimp" do 5 | let(:subscription) { double(:subscription, needs_update?: true) } 6 | 7 | before do 8 | subscription.should_receive(:subscribe) 9 | Spree::Chimpy::Subscription.should_receive(:new).at_least(1).and_return(subscription) 10 | @user = create(:user_with_subscribe_option) 11 | end 12 | 13 | # it "submits after saving" do 14 | # subscription.should_receive(:resubscribe) 15 | # @user.save 16 | # end 17 | 18 | it "submits after destroy" do 19 | subscription.should_receive(:unsubscribe) 20 | @user.destroy 21 | end 22 | end 23 | 24 | context "defaults" do 25 | it "subscribed by default" do 26 | Spree::Chimpy::Config.subscribed_by_default = true 27 | expect(described_class.new.subscribed).to be_truthy 28 | end 29 | 30 | it "doesnt subscribe by default" do 31 | Spree::Chimpy::Config.subscribed_by_default = false 32 | expect(described_class.new.subscribed).to be_falsey 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/models/order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Order do 4 | let(:key) { '857e2096b21e5eb385b9dce2add84434-us14' } 5 | let(:order) { create(:completed_order_with_totals) } 6 | 7 | it 'has a source' do 8 | order = Spree::Order.new 9 | expect(order).to respond_to(:source) 10 | end 11 | 12 | context 'notifying mail chimp' do 13 | before do 14 | Spree::Chimpy::Config.key = nil 15 | 16 | @not_completed_order = create(:order) 17 | 18 | Spree::Chimpy::Config.key = key 19 | end 20 | 21 | subject { Spree::Chimpy } 22 | 23 | it 'doesnt update when order is not completed' do 24 | expect(subject).to_not receive(:enqueue) 25 | @not_completed_order.update! 26 | end 27 | 28 | it 'updates when order is completed' do 29 | new_order = create(:completed_order_with_pending_payment, state: 'confirm') 30 | expect(subject).to receive(:enqueue).with(:order, new_order) 31 | new_order.next 32 | end 33 | 34 | it 'sync when order is completed' do 35 | expect(subject).to receive(:enqueue).with(:order, order) 36 | order.cancel! 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/requests/subscribers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Subscribers' do 4 | 5 | context 'with valid subscription' do 6 | it 'redirects to referer' do 7 | post '/subscribers', 8 | { chimpy_subscriber: { email: 'foo2@bar.com', subscribed: true } }, 9 | { referer: 'http://foo.bar' } 10 | 11 | expect(response).to be_redirect 12 | expect(response.location).to eq('http://foo.bar') 13 | end 14 | 15 | it 'redirects to root URL if no referer' do 16 | post '/subscribers', 17 | { chimpy_subscriber: { email: 'foo2@bar.com', subscribed: true } }, 18 | { referer: nil } 19 | 20 | expect(response).to be_redirect 21 | expect(response.location).to eq('http://www.example.com/') 22 | end 23 | end 24 | 25 | context 'with json response' do 26 | it 'returns 200 with json data' do 27 | post '/subscribers', format: :json, chimpy_subscriber: { email: 'foo2@bar.com', subscribed: true } 28 | 29 | expect(response).to be_success 30 | json_response = JSON.parse(response.body) 31 | expect(json_response['email']).to eq('foo2@bar.com') 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/spree/chimpy/subscription.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | class Subscription 3 | delegate :configured?, :enqueue, to: Spree::Chimpy 4 | 5 | def initialize(model) 6 | @model = model 7 | end 8 | 9 | def subscribe 10 | return unless configured? 11 | defer(:subscribe) if subscribing? 12 | end 13 | 14 | def unsubscribe 15 | return unless configured? 16 | defer(:unsubscribe) if unsubscribing? 17 | end 18 | 19 | def resubscribe(&block) 20 | block.call if block 21 | 22 | return unless configured? 23 | 24 | if unsubscribing? 25 | defer(:unsubscribe) 26 | elsif subscribing? || merge_vars_changed? 27 | defer(:subscribe) 28 | end 29 | end 30 | 31 | private 32 | def defer(event) 33 | enqueue(event, @model) 34 | end 35 | 36 | def subscribing? 37 | @model.subscribed && (@model.subscribed_changed? || @model.id_changed? || @model.new_record?) 38 | end 39 | 40 | def unsubscribing? 41 | !@model.new_record? && !@model.subscribed && @model.subscribed_changed? 42 | end 43 | 44 | def merge_vars_changed? 45 | Config.merge_vars.values.any? do |attr| 46 | name = "#{attr}_changed?".to_sym 47 | !@model.methods.include?(name) || @model.send(name) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/features/spree/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Chimpy', :js do 4 | background do 5 | visit '/signup' 6 | end 7 | 8 | scenario 'guest subscription deface data-hook confirmation' do 9 | page.find('#footer-right') 10 | end 11 | 12 | scenario 'user subscription with opt_in' do 13 | subscribe! 14 | 15 | expect(current_path).to eq spree.root_path 16 | expect(page).to have_selector '.notice', text: 'Welcome! You have signed up successfully.' 17 | expect(Spree::User.count).to be(1) 18 | expect(Spree::User.first.subscribed).to be_truthy 19 | end 20 | 21 | scenario 'user subscription with opt_out' do 22 | skip 'does this refer to the double opt_in/out?' 23 | subscribe! 24 | 25 | expect(current_path).to eq spree.root_path 26 | expect(page).to have_selector '.notice', text: 'Welcome! You have signed up successfully.' 27 | expect(Spree::User.count).to be(1) 28 | expect(Spree::User.first.subscribed).to be_falsey 29 | end 30 | 31 | def subscribe! 32 | expect(page).to have_text 'Sign me up to the newsletter' 33 | 34 | fill_in 'Email', with: Faker::Internet.email 35 | fill_in 'Password', with: 'secret123' 36 | fill_in 'Password Confirmation', with: 'secret123' 37 | 38 | check 'Sign me up to the newsletter' 39 | 40 | expect(page.has_checked_field?('spree_user_subscribed')).to be_truthy 41 | click_button 'Create' 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spree_chimpy.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | Gem::Specification.new do |s| 3 | s.platform = Gem::Platform::RUBY 4 | s.name = 'spree_chimpy' 5 | s.version = '2.0.0.alpha' 6 | s.summary = 'MailChimp/Spree integration using the mailchimp gem' 7 | s.description = s.summary 8 | s.required_ruby_version = '>= 1.9.3' 9 | 10 | s.author = 'Joshua Nussbaum' 11 | s.email = 'josh@godynamo.com' 12 | s.homepage = 'http://www.godynamo.com' 13 | s.license = %q{BSD-3} 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- spec/*`.split("\n") 17 | s.require_path = 'lib' 18 | s.requirements << 'none' 19 | 20 | s.add_dependency 'spree_core', '~> 2.1' 21 | s.add_dependency 'gibbon', '~> 2.2' 22 | 23 | s.add_development_dependency 'rspec-rails', '~> 2.14' 24 | s.add_development_dependency 'rubocop' 25 | s.add_development_dependency 'capybara', '~> 2.2.1' 26 | s.add_development_dependency 'poltergeist' 27 | s.add_development_dependency 'factory_girl', '~> 4.4' 28 | s.add_development_dependency 'shoulda-matchers', '~> 2.5' 29 | s.add_development_dependency 'sqlite3', '~> 1.3.9' 30 | s.add_development_dependency 'simplecov', '0.7.1' 31 | s.add_development_dependency 'database_cleaner', '1.2.0' 32 | s.add_development_dependency 'coffee-rails', '~> 4.0.1' 33 | s.add_development_dependency 'sass-rails', '~> 4.0.2' 34 | s.add_development_dependency 'ffaker' 35 | s.add_development_dependency 'launchy' 36 | end 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_filter 'spec' 4 | add_group 'Controllers', 'app/controllers' 5 | add_group 'Models', 'app/models' 6 | add_group 'Overrides', 'app/overrides' 7 | add_group 'Libraries', 'lib' 8 | end 9 | 10 | ENV['RAILS_ENV'] = 'test' 11 | 12 | 13 | require File.expand_path('../dummy/config/environment.rb', __FILE__) 14 | require 'spree/testing_support/url_helpers' 15 | 16 | require 'rspec/rails' 17 | require 'capybara/rspec' 18 | require 'capybara/rails' 19 | require 'shoulda-matchers' 20 | require 'ffaker' 21 | require 'capybara/poltergeist' 22 | 23 | Capybara.javascript_driver = :poltergeist 24 | 25 | 26 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } 27 | RSpec.configure do |config| 28 | config.infer_spec_type_from_file_location! 29 | # == URL Helpers 30 | # 31 | # Allows access to Spree's routes in specs: 32 | # 33 | # visit spree.admin_path 34 | # current_path.should eql(spree.products_path) 35 | config.include Spree::TestingSupport::UrlHelpers 36 | 37 | # == Mock Framework 38 | # 39 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 40 | # 41 | # config.mock_with :mocha 42 | # config.mock_with :flexmock 43 | # config.mock_with :rr 44 | config.mock_with :rspec 45 | config.use_transactional_fixtures = false 46 | config.treat_symbols_as_metadata_keys_with_true_values = true 47 | config.filter_run :focus 48 | config.run_all_when_everything_filtered = true 49 | end 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Joshua Nussbaum and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name Spree nor the names of its contributors may be used to 13 | endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /lib/generators/templates/spree_chimpy.rb: -------------------------------------------------------------------------------- 1 | Spree::Chimpy.config do |config| 2 | # your API key as provided by MailChimp 3 | # config.key = 'your-api-key' 4 | 5 | # extra api options for the Mailchimp gem 6 | # config.api_options = { throws_exceptions: false, timeout: 3600 } 7 | 8 | # list_id of the list you want to use. 9 | # These ID's can be found by visiting your list in the Mailchimp admin, 10 | # clicking on the settings tab, then the list names and defaults option. 11 | # config.list_id = 'some_list_id' 12 | 13 | # Allow users to be subscribed by default. Defaults to false 14 | # If you enable this option, it's strongly advised that your enable 15 | # double_opt_in as well. Abusing this may cause Mailchimp to suspend your account. 16 | # config.subscribed_by_default = false 17 | 18 | # When double-opt is enabled, the user will receive an email 19 | # asking to confirm their subscription. Defaults to false 20 | # config.double_opt_in = false 21 | 22 | # Send a welcome email after subscribing to a list. 23 | # It is recommended to send on wieh double_opt_in is false. 24 | # config.send_welcome_email = true 25 | 26 | # id of your store. max 10 letters. defaults to "spree" 27 | # config.store_id = 'acme' 28 | 29 | # define a list of merge vars: 30 | # - key: a unique name that mail chimp uses. 10 letters max 31 | # - value: the name of any method on the user class. 32 | # make sure to avoid any of these reserved field names: 33 | # http://kb.mailchimp.com/article/i-got-a-message-saying-that-my-list-field-name-is-reserved-and-cant-be-used 34 | # default is {'EMAIL' => :email} 35 | # config.merge_vars = { 36 | # 'EMAIL' => :email, 37 | # 'HAIRCOLOR' => :hair_color 38 | # } 39 | end 40 | -------------------------------------------------------------------------------- /spec/features/spree/admin/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Chimpy Admin', :js do 4 | stub_authorization! 5 | 6 | given(:user) { create(:user) } 7 | 8 | background do 9 | visit spree.admin_path 10 | end 11 | 12 | scenario 'new user subscription with subscription checked' do 13 | click_link 'Users' 14 | click_link 'New User' 15 | 16 | fill_in_subscription user 17 | 18 | check 'user_subscribed' 19 | click_button 'Create' 20 | 21 | expect(current_path).to eq spree.admin_users_path 22 | expect(page).to have_text 'API ACCESS' 23 | expect(page).to have_text 'NO KEY' 24 | 25 | find_button('Generate API key').click 26 | 27 | fill_in_subscription user 28 | 29 | check 'user_subscribed' 30 | click_button 'Update' 31 | 32 | expect(Spree::User.last.subscribed).to be_truthy 33 | end 34 | 35 | scenario 'new user subscription with subscription un-checked' do 36 | click_link 'Users' 37 | click_link 'New User' 38 | 39 | fill_in_subscription user 40 | 41 | click_button 'Create' 42 | 43 | current_path.should eq spree.admin_users_path 44 | expect(page).to have_text 'API ACCESS' 45 | expect(page).to have_text 'NO KEY' 46 | 47 | find_button('Generate API key').click 48 | 49 | fill_in_subscription user 50 | 51 | click_button 'Update' 52 | 53 | expect(Spree::User.last.subscribed).to be_falsey 54 | end 55 | 56 | private 57 | 58 | def fill_in_subscription(user) 59 | expect(page).to have_text 'SUBSCRIBED TO NEWSLETTER' 60 | 61 | fill_in 'user_email', with: "new-#{user.email}" 62 | fill_in 'user_password', with: 'test123456' 63 | fill_in 'user_password_confirmation', with: 'test123456' 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/spree/chimpy/engine.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | class Engine < Rails::Engine 3 | require 'spree/core' 4 | isolate_namespace Spree 5 | engine_name 'spree_chimpy' 6 | 7 | config.autoload_paths += %W(#{config.root}/lib) 8 | 9 | initializer "spree_chimpy.environment", before: :load_config_initializers do |app| 10 | Spree::Chimpy::Config = Spree::Chimpy::Configuration.new 11 | end 12 | 13 | initializer 'spree_chimpy.ensure' do 14 | if !Rails.env.test? && Spree::Chimpy.configured? 15 | Spree::Chimpy.ensure_list 16 | Spree::Chimpy.ensure_segment 17 | end 18 | end 19 | 20 | initializer 'spree_chimpy.double_opt_in' do 21 | if Spree::Chimpy::Config.subscribed_by_default && !Spree::Chimpy::Config.double_opt_in 22 | Rails.logger.warn("spree_chimpy: You have 'subscribed by default' enabled while 'double opt-in' is disabled. This is not recommended.") 23 | end 24 | end 25 | 26 | initializer 'spree_chimpy.subscribe' do 27 | ActiveSupport::Notifications.subscribe /^spree\.chimpy\./ do |name, start, finish, id, payload| 28 | Spree::Chimpy.handle_event(name.split('.').last, payload) 29 | end 30 | end 31 | 32 | def self.activate 33 | if defined?(Spree::StoreController) 34 | Spree::StoreController.send(:include, Spree::Chimpy::ControllerFilters) 35 | else 36 | Spree::BaseController.send(:include, Spree::Chimpy::ControllerFilters) 37 | end 38 | 39 | # for those shops that use the api controller 40 | if defined?(Spree::Api::BaseController) 41 | Spree::Api::BaseController.send(:include, Spree::Chimpy::ControllerFilters) 42 | end 43 | 44 | Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c| 45 | Rails.configuration.cache_classes ? require(c) : load(c) 46 | end 47 | end 48 | 49 | config.to_prepare &method(:activate).to_proc 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/spree/chimpy/interface/customer_upserter.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Interface 3 | class CustomerUpserter 4 | delegate :log, :store_api_call, to: Spree::Chimpy 5 | 6 | def initialize(order) 7 | @order = order 8 | end 9 | # CUSTOMER will be pulled first from the MC_EID if present on the order.source 10 | # IF that is not found, customer will be found by our Customer ID 11 | # IF that is not found, customer is created with the order email and our Customer ID 12 | def ensure_customer 13 | # use the one from mail chimp or fall back to the order's email 14 | # happens when this is a new user 15 | customer_id = customer_id_from_eid(@order.source.email_id) if @order.source 16 | customer_id || upsert_customer 17 | end 18 | 19 | def self.mailchimp_customer_id(user_id) 20 | "customer_#{user_id}" 21 | end 22 | 23 | def customer_id_from_eid(mc_eid) 24 | email = Spree::Chimpy.list.email_for_id(mc_eid) 25 | if email 26 | begin 27 | response = store_api_call 28 | .customers 29 | .retrieve(params: { "fields" => "customers.id", "email_address" => email }) 30 | 31 | data = response["customers"].first 32 | data["id"] if data 33 | rescue Gibbon::MailChimpError => e 34 | nil 35 | end 36 | end 37 | end 38 | 39 | private 40 | 41 | def upsert_customer 42 | return unless @order.user_id 43 | 44 | customer_id = self.class.mailchimp_customer_id(@order.user_id) 45 | begin 46 | response = store_api_call 47 | .customers(customer_id) 48 | .retrieve(params: { "fields" => "id,email_address"}) 49 | rescue Gibbon::MailChimpError => e 50 | # Customer Not Found, so create them 51 | response = store_api_call 52 | .customers 53 | .create(body: { 54 | id: customer_id, 55 | email_address: @order.email.downcase, 56 | opt_in_status: Spree::Chimpy::Config.subscribe_to_list || false 57 | }) 58 | end 59 | customer_id 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /spec/lib/chimpy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy do 4 | 5 | context "enabled" do 6 | before do 7 | Spree::Chimpy::Interface::List.stub(new: :list) 8 | Spree::Chimpy::Interface::Orders.stub(new: :orders) 9 | config(key: '1234', list_name: 'Members') 10 | end 11 | 12 | subject { described_class } 13 | 14 | specify { should be_configured } 15 | specify "attributes of Spree::Chimpy when configured" do 16 | expect(subject.list).to eq :list 17 | expect(subject.orders).to eq :orders 18 | end 19 | end 20 | 21 | context "disabled" do 22 | before { config(key: nil) } 23 | 24 | subject { described_class } 25 | 26 | specify { should_not be_configured } 27 | specify "attributes of Spree::Chimpy when not configured" do 28 | expect(subject.list).to be_nil 29 | expect(subject.orders).to be_nil 30 | end 31 | end 32 | 33 | context "sync merge vars" do 34 | let(:interface) { double(:interface) } 35 | 36 | before do 37 | Spree::Chimpy::Interface::List.stub(new: interface) 38 | config(key: '1234', 39 | list_name: 'Members', 40 | merge_vars: {'EMAIL' => :email, 'FNAME' => :first_name, 'LNAME' => :last_name}) 41 | end 42 | 43 | it "adds var for each" do 44 | interface.should_receive(:merge_vars).and_return([]) 45 | interface.should_receive(:add_merge_var).with('FNAME', 'First Name') 46 | interface.should_receive(:add_merge_var).with('LNAME', 'Last Name') 47 | 48 | subject.sync_merge_vars 49 | end 50 | 51 | it "skips vars that exist" do 52 | interface.should_receive(:merge_vars).and_return(%w(EMAIL FNAME)) 53 | interface.should_receive(:add_merge_var).with('LNAME', 'Last Name') 54 | 55 | subject.sync_merge_vars 56 | end 57 | 58 | it "doesnt sync if all exist" do 59 | interface.should_receive(:merge_vars).and_return(%w(EMAIL FNAME LNAME)) 60 | interface.should_not_receive(:add_merge_var) 61 | 62 | subject.sync_merge_vars 63 | end 64 | end 65 | 66 | def config(options = {}) 67 | config = Spree::Chimpy::Configuration.new 68 | config.key = options[:key] 69 | config.list_name = options[:list_name] 70 | config.merge_vars = options[:merge_vars] 71 | config 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/lib/products_interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Interface::Products do 4 | let(:store_api) { double(:store_api) } 5 | let(:customer_id) { "customer_123" } 6 | 7 | let(:product_api) { double(:product_api) } 8 | let(:products_api) { double(:products_api) } 9 | 10 | before(:each) do 11 | allow(Spree::Chimpy).to receive(:store_api_call) { store_api } 12 | allow(store_api).to receive(:products) { products_api } 13 | end 14 | 15 | describe "ensure_products" do 16 | let(:order) { 17 | allow_any_instance_of(Spree::Order).to receive(:notify_mail_chimp).and_return(true) 18 | create(:completed_order_with_totals) 19 | } 20 | 21 | it "ensures each product in the order" do 22 | order.line_items.each do |line_item| 23 | interface = double('products') 24 | described_class.stub(:new).with(line_item.variant) { interface } 25 | expect(interface).to receive(:ensure_product) 26 | end 27 | described_class.ensure_products(order) 28 | end 29 | end 30 | 31 | describe "ensure_product" do 32 | let(:variant) { create(:variant) } 33 | let(:interface) { described_class.new(variant) } 34 | 35 | context "when product does not exist" do 36 | before(:each) do 37 | create(:taxon) 38 | allow(product_api).to receive(:create) 39 | allow(interface).to receive(:product_exists_in_mailchimp?).and_return(false) 40 | end 41 | 42 | it "creates the missing product and variants" do 43 | expect(products_api).to receive(:create) do |h| 44 | product = variant.product 45 | expect(h[:body]).to include({ 46 | id: product.id.to_s, 47 | title: product.name, 48 | handle: product.slug, 49 | }) 50 | expect(h[:body][:url]).to include("/products/#{product.slug}") 51 | expect(h[:body][:variants].count).to eq 1 52 | v = h[:body][:variants].first 53 | expect(v[:id]).to eq variant.id.to_s 54 | expect(v[:title]).to eq product.master.name 55 | expect(v[:sku]).to eq variant.sku 56 | expect(v[:price]).to eq product.master.price 57 | end 58 | 59 | interface.ensure_product 60 | end 61 | end 62 | 63 | context "when product already exists" do 64 | before(:each) do 65 | allow(interface).to receive(:product_exists_in_mailchimp?).and_return(true) 66 | allow(store_api).to receive(:products).and_return(product_api) 67 | end 68 | 69 | it "updates the variant" do 70 | variant_api = double('variant_api') 71 | allow(product_api).to receive(:variants).with(variant.id).and_return(variant_api) 72 | 73 | expect(variant_api).to receive(:upsert) do |h| 74 | product = variant.product 75 | expect(h[:body][:url]).to include("/products/#{product.slug}") 76 | expect(h[:body][:title]).to eq variant.name 77 | expect(h[:body][:sku]).to eq variant.sku 78 | expect(h[:body][:price]).to eq variant.price 79 | expect(h[:body][:id]).to be_nil 80 | end 81 | 82 | interface.ensure_product 83 | end 84 | end 85 | end 86 | end -------------------------------------------------------------------------------- /lib/spree/chimpy/interface/order_upserter.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Interface 3 | class OrderUpserter 4 | delegate :log, :store_api_call, to: Spree::Chimpy 5 | 6 | def initialize(order) 7 | @order = order 8 | end 9 | 10 | def customer_id 11 | @customer_id ||= CustomerUpserter.new(@order).ensure_customer 12 | end 13 | 14 | def upsert 15 | return unless customer_id 16 | 17 | Products.ensure_products(@order) 18 | 19 | perform_upsert 20 | end 21 | 22 | private 23 | 24 | def perform_upsert 25 | data = order_hash 26 | log "Adding order #{@order.number} for #{data[:customer][:id]} with campaign #{data[:campaign_id]}" 27 | begin 28 | find_and_update_order(data) 29 | rescue Gibbon::MailChimpError => e 30 | log "Order #{@order.number} Not Found, creating order" 31 | create_order(data) 32 | end 33 | end 34 | 35 | def find_and_update_order(data) 36 | # retrieval is checks if the order exists and raises a Gibbon::MailChimpError when not found 37 | response = store_api_call.orders(@order.number).retrieve(params: { "fields" => "id" }) 38 | log "Order #{@order.number} exists, updating data" 39 | store_api_call.orders(@order.number).update(body: data) 40 | end 41 | 42 | def create_order(data) 43 | store_api_call 44 | .orders 45 | .create(body: data) 46 | rescue Gibbon::MailChimpError => e 47 | log "Unable to create order #{@order.number}. [#{e.raw_body}]" 48 | end 49 | 50 | def order_variant_hash(line_item) 51 | variant = line_item.variant 52 | { 53 | id: "line_item_#{line_item.id}", 54 | product_id: Products.mailchimp_product_id(variant), 55 | product_variant_id: Products.mailchimp_variant_id(variant), 56 | price: variant.price.to_f, 57 | quantity: line_item.quantity 58 | } 59 | end 60 | 61 | def order_hash 62 | source = @order.source 63 | 64 | lines = @order.line_items.map do |line| 65 | # MC can only associate the order with a single category: associate the order with the category right below the root level taxon 66 | order_variant_hash(line) 67 | end 68 | 69 | data = { 70 | id: @order.number, 71 | lines: lines, 72 | order_total: @order.total.to_f, 73 | financial_status: @order.payment_state || "", 74 | fulfillment_status: @order.shipment_state || "", 75 | currency_code: @order.currency, 76 | processed_at_foreign: @order.completed_at ? @order.completed_at.to_formatted_s(:db) : "", 77 | updated_at_foreign: @order.updated_at.to_formatted_s(:db), 78 | shipping_total: @order.ship_total.to_f, 79 | tax_total: @order.try(:included_tax_total).to_f + @order.try(:additional_tax_total).to_f, 80 | customer: { 81 | id: customer_id 82 | } 83 | } 84 | 85 | if source 86 | data[:campaign_id] = source.campaign_id 87 | end 88 | 89 | data 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/spree/chimpy/interface/products.rb: -------------------------------------------------------------------------------- 1 | module Spree::Chimpy 2 | module Interface 3 | class Products 4 | delegate :log, :store_api_call, to: Spree::Chimpy 5 | include Rails.application.routes.url_helpers 6 | 7 | def initialize(variant) 8 | @variant = variant 9 | @product = variant.product 10 | end 11 | 12 | def self.mailchimp_variant_id(variant) 13 | variant.id.to_s 14 | end 15 | 16 | def self.mailchimp_product_id(variant) 17 | variant.product_id.to_s 18 | end 19 | 20 | def self.ensure_products(order) 21 | order.line_items.each do |line| 22 | new(line.variant).ensure_product 23 | end 24 | end 25 | 26 | def ensure_product 27 | if product_exists_in_mailchimp? 28 | upsert_variants 29 | else 30 | store_api_call 31 | .products 32 | .create(body: product_hash) 33 | end 34 | end 35 | 36 | private 37 | 38 | def upsert_variants 39 | all_variants = @product.variants.any? ? @product.variants : [@product.master] 40 | all_variants.each do |v| 41 | data = self.class.variant_hash(v) 42 | data.delete(:id) 43 | 44 | store_api_call 45 | .products(v.product_id) 46 | .variants(v.id) 47 | .upsert(body: data) 48 | end 49 | end 50 | 51 | def product_exists_in_mailchimp? 52 | response = store_api_call 53 | .products(@variant.product.id) 54 | .retrieve(params: { "fields" => "id" }) 55 | !response["id"].nil? 56 | rescue Gibbon::MailChimpError => e 57 | false 58 | end 59 | 60 | def product_hash 61 | root_taxon = Spree::Taxon.where(parent_id: nil).take 62 | taxon = @product.taxons.map(&:self_and_ancestors).flatten.uniq.detect { |t| t.parent == root_taxon } 63 | 64 | # assign a default taxon if the product is not associated with a category 65 | taxon = root_taxon if taxon.blank? 66 | 67 | all_variants = @product.variants.any? ? @product.variants : [@product.master] 68 | data = { 69 | id: self.class.mailchimp_product_id(@variant), 70 | title: @product.name, 71 | handle: @product.slug, 72 | url: self.class.product_url_or_default(@product), 73 | variants: all_variants.map { |v| self.class.variant_hash(v) }, 74 | type: taxon.name 75 | } 76 | 77 | if @product.images.any? 78 | data[:image_url] = @product.images.first.attachment.url(:product) 79 | end 80 | 81 | if @product.respond_to?(:available_on) && @product.available_on 82 | data[:published_at_foreign] = @product.available_on.to_formatted_s(:db) 83 | end 84 | data 85 | end 86 | 87 | def self.variant_hash(variant) 88 | { 89 | id: mailchimp_variant_id(variant), 90 | title: variant.name, 91 | sku: variant.sku, 92 | url: product_url_or_default(variant.product), 93 | price: variant.price.to_f, 94 | image_url: variant_image_url(variant), 95 | inventory_quantity: variant.total_on_hand == Float::INFINITY ? 999 : variant.total_on_hand 96 | } 97 | end 98 | 99 | def self.variant_image_url(variant) 100 | if variant.images.any? 101 | variant.images.first.attachment.url(:product) 102 | elsif variant.product.images.any? 103 | variant.product.images.first.attachment.url(:product) 104 | end 105 | end 106 | 107 | def self.product_url_or_default(product) 108 | if self.respond_to?(:product_url) 109 | product_url(product) 110 | else 111 | URI::HTTP.build({ 112 | host: Rails.application.routes.default_url_options[:host], 113 | :path => "/products/#{product.slug}"} 114 | ).to_s 115 | end 116 | end 117 | end 118 | end 119 | end -------------------------------------------------------------------------------- /lib/spree_chimpy.rb: -------------------------------------------------------------------------------- 1 | require 'spree_core' 2 | require 'spree/chimpy/engine' 3 | require 'spree/chimpy/subscription' 4 | require 'spree/chimpy/workers/delayed_job' 5 | require 'gibbon' 6 | require 'coffee_script' 7 | 8 | module Spree::Chimpy 9 | extend self 10 | 11 | def config(&block) 12 | yield(Spree::Chimpy::Config) 13 | end 14 | 15 | def enqueue(event, object) 16 | payload = {class: object.class.name, id: object.id, object: object} 17 | ActiveSupport::Notifications.instrument("spree.chimpy.#{event}", payload) 18 | end 19 | 20 | def log(message) 21 | Rails.logger.info "spree_chimpy: #{message}" 22 | end 23 | 24 | def configured? 25 | Config.key.present? && (Config.list_name.present? || Config.list_id.present?) 26 | end 27 | 28 | def reset 29 | @list = @orders = nil 30 | end 31 | 32 | def api 33 | Gibbon::Request.new({ api_key: Config.key }.merge(Config.api_options)) if configured? 34 | end 35 | 36 | def store_api_call 37 | Spree::Chimpy.api.ecommerce.stores(Spree::Chimpy::Config.store_id) 38 | end 39 | 40 | def list 41 | @list ||= Interface::List.new(Config.list_name, 42 | Config.customer_segment_name, 43 | Config.double_opt_in, 44 | Config.send_welcome_email, 45 | Config.list_id) if configured? 46 | end 47 | 48 | def orders 49 | @orders ||= Interface::Orders.new if configured? 50 | end 51 | 52 | def list_exists? 53 | list.list_id 54 | end 55 | 56 | def segment_exists? 57 | list.segment_id 58 | end 59 | 60 | def create_segment 61 | list.create_segment 62 | end 63 | 64 | def sync_merge_vars 65 | existing = list.merge_vars + %w(EMAIL) 66 | merge_vars = Config.merge_vars.except(*existing) 67 | 68 | merge_vars.each do |tag, method| 69 | list.add_merge_var(tag.upcase, method.to_s.humanize.titleize) 70 | end 71 | end 72 | 73 | def merge_vars(model) 74 | attributes = Config.merge_vars.except('EMAIL') 75 | 76 | array = attributes.map do |tag, method| 77 | value = model.send(method) if model.methods.include?(method) 78 | 79 | [tag, value.to_s] 80 | end 81 | 82 | Hash[array] 83 | end 84 | 85 | def ensure_list 86 | if Config.list_name.present? 87 | Rails.logger.error("spree_chimpy: hmm.. a list named `#{Config.list_name}` was not found. Please add it and reboot the app") unless list_exists? 88 | end 89 | if Config.list_id.present? 90 | Rails.logger.error("spree_chimpy: hmm.. a list with ID `#{Config.list_id}` was not found. Please add it and reboot the app") unless list_exists? 91 | end 92 | end 93 | 94 | def ensure_segment 95 | if list_exists? && !segment_exists? 96 | create_segment 97 | Rails.logger.error("spree_chimpy: hmm.. a static segment named `#{Config.customer_segment_name}` was not found. Creating it now") 98 | end 99 | end 100 | 101 | def handle_event(event, payload = {}) 102 | payload[:event] = event 103 | 104 | case 105 | when defined?(::Delayed::Job) 106 | ::Delayed::Job.enqueue(payload_object: Spree::Chimpy::Workers::DelayedJob.new(payload), 107 | run_at: Proc.new { 4.minutes.from_now }) 108 | when defined?(::Sidekiq) 109 | Spree::Chimpy::Workers::Sidekiq.perform_in(4.minutes, payload.except(:object)) 110 | when defined?(::Resque) 111 | ::Resque.enqueue(Spree::Chimpy::Workers::Resque, payload.except(:object)) 112 | else 113 | perform(payload) 114 | end 115 | end 116 | 117 | def perform(payload) 118 | return unless configured? 119 | 120 | event = payload[:event].to_sym 121 | object = payload[:object] || payload[:class].constantize.find(payload[:id]) 122 | 123 | case event 124 | when :order 125 | orders.sync(object) 126 | when :subscribe 127 | list.subscribe(object.email, merge_vars(object), customer: object.is_a?(Spree.user_class)) 128 | when :unsubscribe 129 | list.unsubscribe(object.email) 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/lib/order_upserter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Interface::OrderUpserter do 4 | let(:store_id) { "super-store" } 5 | let(:store_api) { double(:store_api) } 6 | let(:order_api) { double(:order_api) } 7 | let(:orders_api) { double(:orders_api) } 8 | let(:customer_id) { "customer_123" } 9 | 10 | before(:each) do 11 | allow(Spree::Chimpy).to receive(:store_api_call) { store_api } 12 | end 13 | 14 | def create_order(options={}) 15 | user = create(:user, email: options[:email]) 16 | 17 | # we need to have a saved order in order to have a non-nil order number 18 | # we need to stub :notify_mail_chimp otherwise sync will be called on the order on update! 19 | allow_any_instance_of(Spree::Order).to receive(:notify_mail_chimp).and_return(true) 20 | order = create(:completed_order_with_totals, user: user, email: options[:email]) 21 | order.source = Spree::Chimpy::OrderSource.new(email_id: options[:email_id], campaign_id: options[:campaign_id]) 22 | order.save 23 | order 24 | end 25 | 26 | describe "#upsert" do 27 | let(:order) { create_order(email_id: 'id-abcd', campaign_id: '1234', email: 'user@example.com') } 28 | let(:interface) { described_class.new(order) } 29 | let(:customer_upserter) { double('cusotmer_upserter') } 30 | 31 | def check_hash(h, expected_customer_id) 32 | body = h[:body] 33 | expect(body[:id]).to eq order.number 34 | 35 | expect(body[:campaign_id]).to eq '1234' 36 | expect(body[:order_total]).to eq order.total.to_f 37 | expect(body[:customer]).to eq({id: expected_customer_id}) 38 | 39 | line = body[:lines].first 40 | item = order.line_items.first 41 | expect(line[:id]).to eq "line_item_#{item.id}" 42 | expect(line[:product_id]).to eq item.variant.product_id.to_s 43 | expect(line[:product_variant_id]).to eq item.variant_id.to_s 44 | expect(line[:price]).to eq item.variant.price.to_f 45 | expect(line[:quantity]).to eq item.quantity 46 | end 47 | 48 | before(:each) do 49 | allow(store_api).to receive(:orders) 50 | .and_return(orders_api) 51 | allow(store_api).to receive(:orders) 52 | .with(anything) 53 | .and_return(order_api) 54 | allow(Spree::Chimpy::Interface::Products).to receive(:ensure_products) 55 | allow(Spree::Chimpy::Interface::CustomerUpserter).to receive(:new).with(order) { customer_upserter } 56 | allow(customer_upserter).to receive(:ensure_customer) { customer_id } 57 | end 58 | 59 | it "calls ensure_products" do 60 | allow(interface).to receive(:perform_upsert) 61 | expect(Spree::Chimpy::Interface::Products).to receive(:ensure_products).with(order) 62 | interface.upsert 63 | end 64 | 65 | it "ensures the customer exists and uses that ID" do 66 | expect(customer_upserter).to receive(:ensure_customer) 67 | .and_return("customer_1") 68 | 69 | expect(interface).to receive(:find_and_update_order) do |h| 70 | expect(h[:customer][:id]).to eq "customer_1" 71 | end 72 | 73 | interface.upsert 74 | end 75 | 76 | it "does not perform the order upsert if no customer_id exists" do 77 | expect(customer_upserter).to receive(:ensure_customer) 78 | .and_return(nil) 79 | 80 | expect(interface).to_not receive(:perform_upsert) 81 | 82 | interface.upsert 83 | end 84 | 85 | context "when order already exists" do 86 | before(:each) do 87 | allow(order_api).to receive(:retrieve) 88 | .and_return({ "id" => order.number }) 89 | end 90 | 91 | it "updates a found order" do 92 | expect(order_api).to receive(:update) do |h| 93 | check_hash(h, customer_id) 94 | end 95 | interface.upsert 96 | end 97 | end 98 | 99 | context "when order is not found" do 100 | before(:each) do 101 | allow(order_api).to receive(:retrieve) 102 | .and_raise(Gibbon::MailChimpError) 103 | end 104 | 105 | it "creates order" do 106 | expect(orders_api).to receive(:create) do |h| 107 | check_hash(h, customer_id) 108 | end 109 | interface.upsert 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/tasks/spree_chimpy.rake: -------------------------------------------------------------------------------- 1 | namespace :spree_chimpy do 2 | namespace :merge_vars do 3 | desc 'sync merge vars with mail chimp' 4 | task :sync do 5 | Spree::Chimpy.sync_merge_vars 6 | end 7 | end 8 | 9 | namespace :orders do 10 | desc 'sync all orders with mail chimp' 11 | task sync: :environment do 12 | scope = Spree::Order.complete 13 | 14 | puts "Exporting #{scope.count} orders" 15 | 16 | scope.find_in_batches do |batch| 17 | print '.' 18 | batch.each do |order| 19 | begin 20 | order.notify_mail_chimp 21 | rescue => exception 22 | if defined?(::Delayed::Job) 23 | raise exception 24 | else 25 | puts exception 26 | end 27 | end 28 | end 29 | end 30 | 31 | puts nil, 'done' 32 | end 33 | end 34 | 35 | namespace :users do 36 | desc 'segment all subscribed users' 37 | task segment: :environment do 38 | if Spree::Chimpy.segment_exists? 39 | emails = Spree.user_class.where(subscribed: true).pluck(:email) 40 | puts "Segmenting all subscribed users" 41 | response = Spree::Chimpy.list.segment(emails) 42 | response["errors"].try :each do |error| 43 | puts "Error #{error["code"]} with email: #{error["email"]} \n msg: #{error["msg"]}" 44 | end 45 | puts "segmented #{response["success"] || 0} out of #{emails.size}" 46 | puts "done" 47 | end 48 | end 49 | 50 | desc "sync all users from mailchimp" 51 | task sync_from_mailchimp: :environment do 52 | puts "Syncing users with data from Mailchimp" 53 | 54 | list = Spree::Chimpy.list 55 | 56 | emails_and_statuses = emails_and_statuses_for_list list 57 | 58 | puts "Found #{emails_and_statuses.count} members to update." 59 | 60 | grouped_emails = emails_and_statuses.group_by { |m| m["status"] } 61 | 62 | { 63 | "subscribed" => true, 64 | "unsubscribed" => false, 65 | }.each do |status, subscribed_db_value| 66 | emails_to_update = grouped_emails[status].map { |m| m["email_address"] } 67 | puts "Setting #{emails_to_update.count} emails to #{status}" 68 | Spree.user_class.where(email: emails_to_update). 69 | update_all(subscribed: subscribed_db_value) 70 | end 71 | end 72 | end 73 | 74 | # Iterate over list members, return a list of hashes with member information. 75 | # returns: [ 76 | # {"email_address" => "xxx@example.com", "status" => "subscribed"}, 77 | # ..., ..., 78 | # ] 79 | def emails_and_statuses_for_list(list) 80 | fields = %w(email_address members.status total_items) 81 | # YMMV, but given we are fetching a small number of fields, this is likely 82 | # safe. 83 | chunk_size = 5_000 84 | 85 | members = [] 86 | total_items = nil 87 | list_params = { params: { 88 | fields: fields.join(","), 89 | count: chunk_size, 90 | offset: 0, 91 | } } 92 | 93 | # make the first request, and continue to iterate until we have all 94 | while total_items.nil? || members.count < total_items 95 | # useful if you want to debug the pagination 96 | # pp total_items, list_params 97 | 98 | # safety check! 99 | if total_items.present? && list_params[:params][:offset] > total_items 100 | fail "Fencepost error, unable to fetch all members. This may be due "\ 101 | "to changes in list size while iterating over it. Please try "\ 102 | "again at a less busy time." 103 | end 104 | # execute the query 105 | response = list.api_list_call.members.retrieve list_params 106 | # capture the results of this chunk 107 | members += response["members"] 108 | 109 | # update pagination tracking 110 | if total_items.nil? 111 | total_items = response["total_items"] 112 | elsif total_items != response["total_items"] 113 | warn "Total items shifted during pagination. To ensure compelte data "\ 114 | "you may want to re-run the script." 115 | end 116 | 117 | # update query parameters for next chunk 118 | list_params[:params][:offset] += chunk_size 119 | end 120 | 121 | members 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/spree/chimpy/interface/list.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | 3 | module Spree::Chimpy 4 | module Interface 5 | class List 6 | delegate :log, to: Spree::Chimpy 7 | 8 | def initialize(list_name, segment_name, double_opt_in, send_welcome_email, list_id) 9 | @list_id = list_id 10 | @segment_name = segment_name 11 | @double_opt_in = double_opt_in 12 | @send_welcome_email = send_welcome_email 13 | @list_name = list_name 14 | end 15 | 16 | def api_call(list_id = nil) 17 | if list_id 18 | Spree::Chimpy.api.lists(list_id) 19 | else 20 | Spree::Chimpy.api.lists 21 | end 22 | end 23 | 24 | def subscribe(email, merge_vars = {}, options = {}) 25 | log "Subscribing #{email} to #{@list_name}" 26 | 27 | begin 28 | api_member_call(email) 29 | .upsert(body: { 30 | email_address: email, 31 | status: "subscribed", 32 | merge_fields: merge_vars, 33 | email_type: 'html' 34 | }) #, @double_opt_in, true, true, @send_welcome_email) 35 | 36 | segment([email]) if options[:customer] 37 | rescue Gibbon::MailChimpError => ex 38 | log "Subscriber #{email} rejected for reason: [#{ex.raw_body}]" 39 | true 40 | end 41 | end 42 | 43 | def unsubscribe(email) 44 | log "Unsubscribing #{email} from #{@list_name}" 45 | 46 | begin 47 | api_member_call(email) 48 | .update(body: { 49 | email_address: email, 50 | status: "unsubscribed" 51 | }) 52 | rescue Gibbon::MailChimpError => ex 53 | log "Subscriber unsubscribe for #{email} failed for reason: [#{ex.raw_body}]" 54 | true 55 | end 56 | end 57 | 58 | def email_for_id(mc_eid) 59 | log "Checking customer id for #{mc_eid} from #{@list_name}" 60 | begin 61 | response = api_list_call 62 | .members 63 | .retrieve(params: { "unique_email_id" => mc_eid, "fields" => "members.id,members.email_address" }) 64 | 65 | member_data = response["members"].first 66 | member_data["email_address"] if member_data 67 | rescue Gibbon::MailChimpError => ex 68 | nil 69 | end 70 | end 71 | 72 | def info(email) 73 | log "Checking member info for #{email} from #{@list_name}" 74 | 75 | #maximum of 50 emails allowed to be passed in 76 | begin 77 | response = api_member_call(email) 78 | .retrieve(params: { "fields" => "email_address,merge_fields,status"}) 79 | 80 | response = response.symbolize_keys 81 | response.merge(email: response[:email_address]) 82 | rescue Gibbon::MailChimpError 83 | {} 84 | end 85 | 86 | end 87 | 88 | def merge_vars 89 | log "Finding merge vars for #{@list_name}" 90 | 91 | response = api_list_call 92 | .merge_fields 93 | .retrieve(params: { "fields" => "merge_fields.tag,merge_fields.name"}) 94 | response["merge_fields"].map { |record| record['tag'] } 95 | end 96 | 97 | def add_merge_var(tag, description) 98 | log "Adding merge var #{tag} to #{@list_name}" 99 | 100 | api_list_call 101 | .merge_fields 102 | .create(body: { 103 | tag: tag, 104 | name: description, 105 | type: "text" 106 | }) 107 | end 108 | 109 | def find_list_id(name) 110 | response = api_call 111 | .retrieve(params: {"fields" => "lists.id,lists.name"}) 112 | list = response["lists"].detect { |r| r["name"] == name } 113 | list["id"] if list 114 | end 115 | 116 | def list_id 117 | @list_id ||= find_list_id(@list_name) 118 | end 119 | 120 | def segment(emails = []) 121 | log "Adding #{emails} to segment #{@segment_name} [#{segment_id}] in list [#{list_id}]" 122 | 123 | api_list_call.segments(segment_id.to_i).create(body: { members_to_add: Array(emails) }) 124 | end 125 | 126 | def create_segment 127 | log "Creating segment #{@segment_name}" 128 | 129 | result = api_list_call.segments.create(body: { name: @segment_name, static_segment: []}) 130 | @segment_id = result["id"] 131 | end 132 | 133 | def find_segment_id 134 | response = api_list_call 135 | .segments 136 | .retrieve(params: {"fields" => "segments.id,segments.name"}) 137 | segment = response["segments"].detect {|segment| segment['name'].downcase == @segment_name.downcase } 138 | 139 | segment['id'] if segment 140 | end 141 | 142 | def segment_id 143 | @segment_id ||= find_segment_id 144 | end 145 | 146 | def api_list_call 147 | api_call(list_id) 148 | end 149 | 150 | def api_member_call(email) 151 | api_list_call.members(email_to_lower_md5(email)) 152 | end 153 | 154 | private 155 | 156 | def email_to_lower_md5(email) 157 | Digest::MD5.hexdigest(email.downcase) 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /spec/lib/customers_interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Interface::CustomerUpserter do 4 | let(:store_api) { double(:store_api) } 5 | let(:customer_api) { double(:customer_api) } 6 | let(:customers_api) { double(:customers_api) } 7 | let(:email_id) { "id-abcd" } 8 | let(:campaign_id) { "campaign-1" } 9 | 10 | let(:order) { 11 | allow_any_instance_of(Spree::Order).to receive(:notify_mail_chimp).and_return(true) 12 | order = create(:completed_order_with_totals) 13 | order.source = Spree::Chimpy::OrderSource.new(email_id: email_id, campaign_id: campaign_id) 14 | order.save 15 | order 16 | } 17 | let(:interface) { described_class.new(order) } 18 | let(:list) { double(:list) } 19 | 20 | before(:each) do 21 | allow(Spree::Chimpy).to receive(:store_api_call) { store_api } 22 | Spree::Chimpy.stub(list: list) 23 | Spree::Chimpy::Config.subscribe_to_list = true 24 | end 25 | 26 | describe ".ensure_customers" do 27 | 28 | #TODO: Changed from skips sync when mismatch - 29 | # Updated logic takes the customer attached to the mc_eid regardless of email matching order 30 | # When no customer exists for that mc_eid, it will create the customer for the order email 31 | # Should this remain due to v3.0 updates? 32 | it "retrieves the customer id from the order source if it exists" do 33 | order.source = Spree::Chimpy::OrderSource.new(email_id: 'id-abcd') 34 | order.save 35 | 36 | allow(interface).to receive(:customer_id_from_eid) 37 | .with('id-abcd') 38 | .and_return("customer_999") 39 | 40 | expect(interface.ensure_customer).to eq "customer_999" 41 | end 42 | 43 | context "when no customer from order source" do 44 | before(:each) do 45 | allow(interface).to receive(:customer_id_from_eid) 46 | .with('id-abcd') 47 | .and_return(nil) 48 | end 49 | 50 | it "upserts the customer" do 51 | allow(interface).to receive(:upsert_customer) { "customer_998" } 52 | 53 | expect(interface.ensure_customer).to eq "customer_998" 54 | end 55 | 56 | it "returns nil if guest checkout" do 57 | order.user_id = nil 58 | expect(interface.ensure_customer).to be_nil 59 | end 60 | end 61 | end 62 | 63 | describe "#upsert_customer" do 64 | 65 | before(:each) do 66 | allow(store_api).to receive(:customers) 67 | .and_return(customers_api) 68 | allow(store_api).to receive(:customers) 69 | .with("customer_#{order.user_id}") 70 | .and_return(customer_api) 71 | end 72 | 73 | it "retrieves based on the customer_id" do 74 | expect(customer_api).to receive(:retrieve) 75 | .with(params: { "fields" => "id,email_address"}) 76 | .and_return({ "id" => "customer_#{order.user_id}", "email_address" => order.email}) 77 | 78 | customer_id = interface.send(:upsert_customer) 79 | expect(customer_id).to eq "customer_#{order.user_id}" 80 | end 81 | 82 | it "creates the customer when lookup fails" do 83 | allow(customer_api).to receive(:retrieve) 84 | .and_raise(Gibbon::MailChimpError) 85 | 86 | expect(customers_api).to receive(:create) 87 | .with(:body => { 88 | id: "customer_#{order.user_id}", 89 | email_address: order.email.downcase, 90 | opt_in_status: true 91 | }) 92 | 93 | customer_id = interface.send(:upsert_customer) 94 | expect(customer_id).to eq "customer_#{order.user_id}" 95 | end 96 | 97 | it "honors subscribe_to_list settings" do 98 | Spree::Chimpy::Config.subscribe_to_list = false 99 | 100 | allow(customer_api).to receive(:retrieve) 101 | .and_raise(Gibbon::MailChimpError) 102 | 103 | expect(customers_api).to receive(:create) do |h| 104 | expect(h[:body][:opt_in_status]).to eq false 105 | end 106 | interface.send(:upsert_customer) 107 | end 108 | end 109 | 110 | describe "#customer_id_from_eid" do 111 | let(:email) { "user@example.com" } 112 | before(:each) do 113 | allow(store_api).to receive(:customers) { customers_api } 114 | end 115 | 116 | it "returns based on the mailchimp email address when found" do 117 | allow(list).to receive(:email_for_id).with("id-abcd") 118 | .and_return(email) 119 | 120 | expect(customers_api).to receive(:retrieve) 121 | .with(params: { "fields" => "customers.id", "email_address" => email}) 122 | .and_return({ "customers" => [{"id" => "customer_xyz"}] }) 123 | 124 | id = interface.customer_id_from_eid("id-abcd") 125 | expect(id).to eq "customer_xyz" 126 | end 127 | 128 | it "is nil if email for id not found" do 129 | allow(list).to receive(:email_for_id).with("id-abcd") 130 | .and_return(nil) 131 | 132 | expect(interface.customer_id_from_eid("id-abcd")).to be_nil 133 | end 134 | 135 | it "is nil if email not found among customers" do 136 | allow(list).to receive(:email_for_id) 137 | .with("id-abcd") 138 | .and_return(email) 139 | 140 | expect(customers_api).to receive(:retrieve) 141 | .and_raise(Gibbon::MailChimpError) 142 | 143 | expect(interface.customer_id_from_eid("id-abcd")).to be_nil 144 | end 145 | end 146 | end -------------------------------------------------------------------------------- /spec/lib/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Subscription do 4 | 5 | context "mail chimp enabled" do 6 | let(:interface) { double(:interface) } 7 | 8 | before do 9 | Spree::Chimpy::Config.list_name = 'Members' 10 | Spree::Chimpy::Config.merge_vars = {'EMAIL' => :email} 11 | Spree::Chimpy.stub(list: interface) 12 | end 13 | 14 | context "subscribing users" do 15 | let(:user) { build(:user, subscribed: true) } 16 | let(:subscription) { described_class.new(user) } 17 | 18 | before do 19 | Spree::Chimpy::Config.merge_vars = {'EMAIL' => :email, 'SIZE' => :size, 'HEIGHT' => :height} 20 | 21 | def user.size 22 | '10' 23 | end 24 | 25 | def user.height 26 | '20' 27 | end 28 | end 29 | 30 | it "subscribes users" do 31 | interface.should_receive(:subscribe).with(user.email, {'SIZE' => '10', 'HEIGHT' => '20'}, customer: true) 32 | user.save 33 | end 34 | end 35 | 36 | context "subscribing subscribers" do 37 | let(:subscriber) { Spree::Chimpy::Subscriber.new(email: "test@example.com", subscribed: true) } 38 | let(:subscription) { described_class.new(subscriber) } 39 | 40 | it "subscribes subscribers" do 41 | interface.should_receive(:subscribe).with(subscriber.email, {}, customer: false) 42 | interface.should_not_receive(:segment) 43 | subscriber.save 44 | end 45 | end 46 | 47 | # context "resubscribe" do 48 | # let(:user) { create(:user, subscribed: true) } 49 | # let(:subscription) { double(:subscription) } 50 | 51 | # before do 52 | # interface.should_receive(:subscribe).once.with(user.email) 53 | # user.stub(subscription: subscription) 54 | # end 55 | 56 | # context "when update needed" do 57 | # it "calls resubscribe" do 58 | # subscription.should_receive(:resubscribe) 59 | # user.save 60 | # end 61 | # end 62 | 63 | # context "when update not needed" do 64 | # it "still calls resubscribe, and does nothing" do 65 | # subscription.should_receive(:resubscribe) 66 | # subscription.should_not_receive(:unsubscribe) 67 | # user.save 68 | # end 69 | # end 70 | # end 71 | 72 | context "unsubscribing" do 73 | let(:subscription) { described_class.new(user) } 74 | 75 | before { interface.stub(:subscribe) } 76 | 77 | context "subscribed user" do 78 | let(:user) { create(:user, subscribed: true) } 79 | it "unsubscribes" do 80 | interface.should_receive(:unsubscribe).with(user.email) 81 | user.subscribed = false 82 | subscription.unsubscribe 83 | end 84 | end 85 | 86 | context "non-subscribed user" do 87 | let(:user) { build(:user, subscribed: false) } 88 | it "does nothing" do 89 | interface.should_not_receive(:unsubscribe) 90 | subscription.unsubscribe 91 | end 92 | end 93 | end 94 | 95 | context "when an existing user is not already subscribed" do 96 | let(:user) { create(:user, subscribed: false) } 97 | let(:subscription) { described_class.new(user) } 98 | 99 | context "#resubscribe" do 100 | it "subscribes the user" do 101 | interface.should_receive(:subscribe).with(user.email, {}, {customer: true}) 102 | user.subscribed = true 103 | subscription.resubscribe 104 | end 105 | end 106 | end 107 | 108 | context "when an existing user is already subscribed" do 109 | let(:user) { create(:user, subscribed: true) } 110 | let(:subscription) { described_class.new(user) } 111 | 112 | before { interface.stub(:subscribe) } 113 | 114 | context "#resubscribe" do 115 | it "unsubscribes the user" do 116 | interface.should_receive(:unsubscribe).with(user.email) 117 | user.subscribed = false 118 | subscription.resubscribe 119 | end 120 | 121 | context "merge vars changed" do 122 | let(:user) { create(:user, subscribed: true, size: 10, height: 20) } 123 | 124 | before do 125 | Spree::Chimpy::Config.merge_vars = {'EMAIL' => :email, 'SIZE' => :size, 'HEIGHT' => :height} 126 | 127 | Spree::User.class_eval do 128 | attr_accessor :size, :height 129 | end 130 | end 131 | 132 | it "subscribes the user once again" do 133 | user.size += 5 134 | user.height += 10 135 | interface.should_receive(:subscribe).with(user.email, {"SIZE"=> user.size.to_s, "HEIGHT"=> user.height.to_s}, {:customer=>true}) 136 | subscription.resubscribe 137 | end 138 | end 139 | end 140 | end 141 | 142 | context "when updating a user and not changing subscription details" do 143 | it "does not update mailchimp" do 144 | interface.stub(:subscribe) 145 | user = create(:user, subscribed: true) 146 | 147 | interface.should_not_receive(:subscribe) 148 | user.spree_api_key = 'something' 149 | user.save! 150 | end 151 | end 152 | 153 | end 154 | 155 | context "mail chimp disabled" do 156 | before do 157 | Spree::Chimpy::Config.stub(key: nil) 158 | 159 | user = build(:user, subscribed: true) 160 | @subscription = described_class.new(user) 161 | end 162 | 163 | specify { @subscription.subscribe } 164 | specify { @subscription.unsubscribe } 165 | specify { @subscription.resubscribe {} } 166 | end 167 | 168 | end 169 | -------------------------------------------------------------------------------- /spec/lib/list_interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Chimpy::Interface::List do 4 | let(:interface) { described_class.new('Members', 'customers', true, true, nil) } 5 | let(:api) { double(:api) } 6 | let(:list_id) { "a3d3" } 7 | let(:segment_id) { 3887 } 8 | let(:mc_eid) { "ef3176d4dd" } 9 | #let(:lists) { double(:lists, [{"name" => "Members", "id" => list_id }] ) } 10 | let(:key) { '857e2096b21e5eb385b9dce2add84434-us14' } 11 | 12 | let(:lists_response) { {"lists"=>[{"id"=>list_id, "name"=>"Members"}]} } 13 | let(:segments_response) { {"segments"=>[{"id"=>segment_id, "name"=>"Customers"}]} } 14 | let(:info_response) { {"email_address"=>email, "merge_fields"=>{"FNAME"=>"Jane", "LNAME"=>"Doe","SIZE" => '10'}} } 15 | let(:merge_response) { {"merge_fields"=>[{"tag"=>"FNAME", "name"=>"First Name"}, {"tag"=>"LNAME", "name"=>"Last Name"}]} } 16 | let(:members_response) { {"members"=> [{"id" => "customer_123", "email_address"=>email, "unique_email_id"=>mc_eid, "email_type"=>"html", "status"=>"subscribed", "merge_fields"=>{"FNAME"=>"", "LNAME"=>"", "SIZE"=>"10"}}] } } 17 | 18 | let(:email) { 'user@example.com' } 19 | 20 | let(:members_api) { double(:members_api) } 21 | let(:member_api) { double(:member_api) } 22 | let(:lists_api) { double(:lists_api) } 23 | let(:list_api) { double(:list_api) } 24 | let(:segments_api) { double(:segments_api) } 25 | let(:segment_api) { double(:segment_api) } 26 | let(:merges_api) { double(:merges_api) } 27 | 28 | before do 29 | Spree::Chimpy::Config.key = key 30 | Gibbon::Request.stub(:new).with({ api_key: key, timeout: 60 }).and_return(api) 31 | 32 | api.stub(:lists).and_return(lists_api) 33 | lists_api.stub(:retrieve).and_return(lists_response) 34 | 35 | api.stub(:lists).with(list_id).and_return(list_api) 36 | 37 | list_api.stub(:members).and_return(members_api) 38 | list_api.stub(:members).with(Digest::MD5.hexdigest(email)).and_return(member_api) 39 | 40 | list_api.stub(:segments).and_return(segments_api) 41 | list_api.stub(:segments).with(segment_id).and_return(segment_api) 42 | 43 | list_api.stub(:merge_fields).and_return(merges_api) 44 | end 45 | 46 | context "#subscribe" do 47 | it "subscribes" do 48 | expect(member_api).to receive(:upsert) 49 | .with(hash_including(body: {email_address: email, status: "subscribed", merge_fields: { 'SIZE' => '10' }, email_type: 'html' })) 50 | interface.subscribe(email, 'SIZE' => '10') 51 | end 52 | 53 | it "ignores exception Gibbon::MailChimpError" do 54 | expect(member_api).to receive(:upsert) 55 | .and_raise Gibbon::MailChimpError 56 | expect(lambda { interface.subscribe(email) }).not_to raise_error 57 | end 58 | end 59 | 60 | context "#unsubscribe" do 61 | it "unsubscribes" do 62 | expect(member_api).to receive(:update).with( 63 | hash_including(body: { email_address: email, status: "unsubscribed" }) 64 | ) 65 | interface.unsubscribe(email) 66 | end 67 | 68 | it "ignores exception Gibbon::MailChimpError" do 69 | expect(member_api).to receive(:update).and_raise Gibbon::MailChimpError 70 | expect(lambda { interface.unsubscribe(email) }).not_to raise_error 71 | end 72 | end 73 | 74 | context "member info" do 75 | it "find when no errors" do 76 | expect(member_api).to receive(:retrieve).with( 77 | { params: { "fields" => "email_address,merge_fields,status" } } 78 | ).and_return(info_response) 79 | expect(interface.info(email)).to include( 80 | email_address: email, 81 | merge_fields: { "FNAME" => "Jane", "LNAME" => "Doe", "SIZE" => '10'} 82 | ) 83 | end 84 | it "adds legacy field email for backwards compatibility" do 85 | expect(member_api).to receive(:retrieve).with( 86 | { params: { "fields" => "email_address,merge_fields,status" } } 87 | ).and_return(info_response) 88 | expect(interface.info(email)).to include(email: email) 89 | end 90 | 91 | it "returns empty hash on error" do 92 | expect(member_api).to receive(:retrieve).and_raise Gibbon::MailChimpError 93 | expect(interface.info("user@example.com")).to eq({}) 94 | end 95 | 96 | describe "email_for_id" do 97 | it "can find the email address for a unique_email_id (mc_eid)" do 98 | expect(members_api).to receive(:retrieve).with( 99 | params: { "unique_email_id" => mc_eid, "fields" => "members.id,members.email_address" } 100 | ).and_return(members_response) 101 | expect(interface.email_for_id(mc_eid)).to eq email 102 | end 103 | it "returns nil when empty array returned" do 104 | expect(members_api).to receive(:retrieve).with( 105 | params: { "unique_email_id" => mc_eid, "fields" => "members.id,members.email_address" } 106 | ).and_return({ "members" => [] }) 107 | expect(interface.email_for_id(mc_eid)).to be_nil 108 | end 109 | it "returns nil on error" do 110 | expect(members_api).to receive(:retrieve).and_raise Gibbon::MailChimpError 111 | expect(interface.email_for_id(mc_eid)).to be_nil 112 | end 113 | end 114 | end 115 | 116 | it "segments users" do 117 | expect(member_api).to receive(:upsert) 118 | .with(hash_including( 119 | body: { 120 | email_address: email, 121 | status: "subscribed", 122 | merge_fields: { 'SIZE' => '10' }, 123 | email_type: 'html' 124 | }) 125 | ) 126 | 127 | expect(segments_api).to receive(:retrieve).with( 128 | params: { "fields" => "segments.id,segments.name"} 129 | ).and_return(segments_response) 130 | 131 | expect(segment_api).to receive(:create).with( 132 | body: { members_to_add: [email] } 133 | ) 134 | interface.subscribe("user@example.com", {'SIZE' => '10'}, {customer: true}) 135 | end 136 | 137 | it "segments" do 138 | emails = ["test@test.nl", "test@test.com"] 139 | expect(segments_api).to receive(:retrieve).with( 140 | params: { "fields" => "segments.id,segments.name"} 141 | ).and_return(segments_response) 142 | 143 | expect(segment_api).to receive(:create).with( 144 | body: { members_to_add: emails } 145 | ) 146 | interface.segment(emails) 147 | end 148 | 149 | it "creates the segment" do 150 | expect(segments_api).to receive(:create).with( 151 | body: { 152 | name: "customers", 153 | static_segment: [] 154 | } 155 | ).and_return({ "id" => 3959 }) 156 | expect(interface.create_segment).to eq 3959 157 | end 158 | 159 | it "find list id" do 160 | expect(interface.list_id).to eq list_id 161 | end 162 | 163 | it "checks if merge var exists" do 164 | expect(merges_api).to receive(:retrieve) 165 | .with(params: { "fields" => "merge_fields.tag,merge_fields.name" }).and_return(merge_response) 166 | expect(interface.merge_vars).to match_array %w(FNAME LNAME) 167 | end 168 | 169 | it "adds a merge var" do 170 | expect(merges_api).to receive(:create).with(body: { 171 | tag: "SIZE", name: "Your Size", type: "text" 172 | }) 173 | interface.add_merge_var('SIZE', 'Your Size') 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spree/MailChimp Integration 2 | 3 | [![Build Status](https://app.wercker.com/status/03e07999926ddaf022b4ad7ec6460f27/s "wercker status")](https://app.wercker.com/project/bykey/03e07999926ddaf022b4ad7ec6460f27) 4 | [![Code Climate](https://codeclimate.com/github/DynamoMTL/spree_chimpy.png)](https://codeclimate.com/github/DynamoMTL/spree_chimpy) 5 | 6 | Makes it easy to integrate your [Spree][1] app with [MailChimp][2]. 7 | 8 | **List synchronization** 9 | > Automatically syncs Spree's user list with MailChimp. The user can 10 | > subscribe/unsubscribe via the registration and account pages. 11 | 12 | **Order synchronoization** 13 | > Fully supports MailChimp's [eCommerce360][3] API. Allows you to 14 | > create targeted campaigns in MailChimp based on a user's purchase history. 15 | > We'll even update MailChimp if the order changes after the 16 | > sale (i.e. order modification, cancelation, return). User's who check out 17 | > with their email in the Spree Storefront, will accrue order data under this 18 | > email in MailChimp. This data will be available under the 'E-Commerce' tab 19 | > for the specific subscriber. 20 | 21 | **Campaign Revenue Tracking** 22 | > Notifies MailChimp when an order originates from a campaign email. 23 | 24 | **Custom User Data** 25 | > Easily add your own custom merge vars. We'll only sync them when data changes. 26 | 27 | **Existing Stores** 28 | > Provides a handy rake task `rake spree_chimpy:orders:sync` is included 29 | > to sync up all your existing order data with mail chimp. Run this after 30 | > installing spree_chimpy to an existing store. 31 | 32 | > Also provides `rake spree_chimpy:users:sync_from_mailchimp` which annotates 33 | > your spree users as being subscribed or not, according to Mailchimp. 34 | 35 | **Deferred Processing** 36 | > Communication between Spree and MailChimp is synchronous by default. If you 37 | > have `delayed_job` in your bundle, the communication is queued up and 38 | > deferred to one of your workers. (`sidekiq` support also planned). 39 | 40 | **Angular.js/Sprangular** 41 | > You can integrate it 42 | > with [sprangular](https://github.com/sprangular/sprangular) by using 43 | > the [sprangular_chimpy](https://github.com/sprangular/sprangular_chimpy) gem. 44 | 45 | ## Installing 46 | 47 | Add spree_chimpy to your `Gemfile`: 48 | 49 | ```ruby 50 | gem 'spree_chimpy' 51 | ``` 52 | 53 | Alternatively you can use the git repo directly: 54 | 55 | ```ruby 56 | gem 'spree_chimpy', github: 'DynamoMTL/spree_chimpy', branch: 'master' 57 | ``` 58 | 59 | Run bundler: 60 | 61 | bundle 62 | 63 | Install migrations & initializer file: 64 | 65 | bundle exec rails g spree_chimpy:install 66 | 67 | --- 68 | 69 | ### MailChimp Setup 70 | 71 | If you don't already have an account, you can [create one here][4] for free. 72 | 73 | Make sure to create a list if you don't already have one. Use any name you like, just dont forget to update the `Spree::Chimpy::Config#list_name` setting. 74 | 75 | ### Spree Setup 76 | 77 | Edit the initializer created by the `spree_chimpy:install` generator. Only the API key is required. 78 | 79 | ```ruby 80 | # config/initializers/spree_chimpy.rb 81 | Spree::Chimpy.config do |config| 82 | # your API key provided by MailChimp 83 | config.key = 'your-api-key' 84 | end 85 | ``` 86 | 87 | If you'd like, you can add additional options: 88 | 89 | ```ruby 90 | # config/initializers/spree_chimpy.rb 91 | Spree::Chimpy.config do |config| 92 | # your API key as provided by MailChimp 93 | config.key = 'your-api-key' 94 | 95 | # name of your list, defaults to "Members" 96 | config.list_name = 'peeps' 97 | 98 | # change the double-opt-in behavior 99 | config.double_opt_in = false 100 | 101 | # send welcome email 102 | config.send_welcome_email = true 103 | 104 | # id of your store. max 10 letters. defaults to "spree" 105 | config.store_id = 'acme' 106 | 107 | # define a list of merge vars: 108 | # - key: a unique name that mail chimp uses. 10 letters max 109 | # - value: the name of any method on the user class. 110 | # default is {'EMAIL' => :email} 111 | config.merge_vars = { 112 | 'EMAIL' => :email, 113 | 'HAIRCOLOR' => :hair_color 114 | } 115 | end 116 | ``` 117 | 118 | When adding custom merge vars, you'll need to notify MailChimp by running the rake task: `rake spree_chimpy:merge_vars:sync` 119 | 120 | For deployment on Heroku, you can configure the API key with environment variables: 121 | 122 | ```ruby 123 | # config/initializers/spree_chimpy.rb 124 | Spree::Chimpy.config do |config| 125 | config.key = ENV['MAILCHIMP_API_KEY'] 126 | end 127 | ``` 128 | 129 | ### Segmenting 130 | 131 | By default spree_chimpy will try to segment customers. The segment name can be configured using the `segment_name` setting. 132 | Spree_chimpy will use an existing segment if it exists. If no segment can be found it will be created for you automatically. 133 | 134 | #### Note about double-opt-in & segmenting 135 | 136 | Mailchimp does not allow you to segment emails that have not confirmed their subscription. This means that if you use the 137 | double-opt-in setting users will not get segmented by default. To work around this there is a rake task to segment all currently subscribed users. 138 | 139 | `rake spree_chimpy:users:segment` 140 | 141 | The output of this command will look something like this: 142 | 143 | Segmenting all subscribed users 144 | Error 215 with email: user@example.com 145 | msg: The email address "user@example" does not belong to this list 146 | segmented 2 out of 3 147 | done 148 | 149 | You can run this task recurring by setting up a cron using [whenever](https://github.com/javan/whenever) or by using [clockwork](https://github.com/tomykaira/clockwork). Alternatively when you host on Heroku you can use [Heroku Scheduler](https://addons.heroku.com/scheduler) 150 | 151 | ### Adding a Guest subscription form 152 | 153 | spree_chimpy comes with a default subscription form for users who are not logged in, just add the following deface override: 154 | 155 | ```ruby 156 | Deface::Override.new(:virtual_path => "spree/shared/_footer", 157 | :name => "spree_chimpy_subscription_form", 158 | :insert_bottom => "#footer-right", 159 | :partial => "spree/shared/guest_subscription") 160 | ``` 161 | 162 | The selector and virtual path can be changed to taste. 163 | 164 | --- 165 | 166 | ## Contributing 167 | 168 | In the spirit of [free software][5], **everyone** is encouraged to help improve this project. 169 | 170 | Here are some ways *you* can contribute: 171 | 172 | * by using prerelease versions 173 | * by reporting [bugs][6] 174 | * by suggesting new features 175 | * by writing translations 176 | * by writing or editing documentation 177 | * by writing specifications 178 | * by writing code (*no patch is too small*: fix typos, add comments, clean up inconsistent whitespace) 179 | * by refactoring code 180 | * by resolving [issues][6] 181 | * by reviewing patches 182 | 183 | Starting point: 184 | 185 | * Fork the repo 186 | * Clone your repo 187 | * Run `bundle install` 188 | * Run `bundle exec rake test_app` to create the test application in `spec/test_app` 189 | * Make your changes 190 | * Ensure specs pass by running `bundle exec rspec spec` 191 | * Submit your pull request 192 | 193 | Copyright (c) 2014 [Joshua Nussbaum][8] and [contributors][9], released under the [New BSD License][7] 194 | 195 | [1]: http://spreecommerce.com 196 | [2]: http://www.mailchimp.com 197 | [3]: http://kb.mailchimp.com/article/what-is-ecommerce360-and-how-does-it-work-with-mailchimp 198 | [4]: https://login.mailchimp.com/signup 199 | [5]: http://www.fsf.org/licensing/essays/free-sw.html 200 | [6]: https://github.com/DynamoMTL/spree_chimpy/issues 201 | [7]: https://github.com/DynamoMTL/spree_chimpy/tree/master/LICENSE.md 202 | [8]: https://github.com/joshnuss 203 | [9]: https://github.com/DynamoMTL/spree_chimpy/contributors 204 | --------------------------------------------------------------------------------