├── .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 | [](https://app.wercker.com/project/bykey/03e07999926ddaf022b4ad7ec6460f27)
4 | [](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 |
--------------------------------------------------------------------------------