solidus_mailchimp_sync 1.0.0.beta01

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +103 -0
  4. data/Rakefile +31 -0
  5. data/app/assets/javascripts/spree/backend/solidus_mailchimp_sync.js +2 -0
  6. data/app/assets/javascripts/spree/frontend/solidus_mailchimp_sync.js +2 -0
  7. data/app/assets/stylesheets/spree/backend/solidus_mailchimp_sync.css +4 -0
  8. data/app/assets/stylesheets/spree/frontend/solidus_mailchimp_sync.css +4 -0
  9. data/app/jobs/solidus_mailchimp_sync/sync_job.rb +15 -0
  10. data/app/models/solidus_mailchimp_sync/error.rb +32 -0
  11. data/app/models/solidus_mailchimp_sync/mailchimp.rb +69 -0
  12. data/app/serializers/solidus_mailchimp_sync/customer_serializer.rb +34 -0
  13. data/app/serializers/solidus_mailchimp_sync/line_item_serializer.rb +26 -0
  14. data/app/serializers/solidus_mailchimp_sync/order_serializer.rb +70 -0
  15. data/app/serializers/solidus_mailchimp_sync/product_serializer.rb +44 -0
  16. data/app/serializers/solidus_mailchimp_sync/variant_serializer.rb +59 -0
  17. data/app/solidus_decorators/spree/image_decorator.rb +16 -0
  18. data/app/solidus_decorators/spree/line_item_decorator.rb +8 -0
  19. data/app/solidus_decorators/spree/order_decorator.rb +8 -0
  20. data/app/solidus_decorators/spree/price_decorator.rb +10 -0
  21. data/app/solidus_decorators/spree/product_decorator.rb +8 -0
  22. data/app/solidus_decorators/spree/user_decorator.rb +10 -0
  23. data/app/solidus_decorators/spree/variant_decorator.rb +8 -0
  24. data/app/synchronizers/solidus_mailchimp_sync/base_synchronizer.rb +74 -0
  25. data/app/synchronizers/solidus_mailchimp_sync/order_synchronizer.rb +108 -0
  26. data/app/synchronizers/solidus_mailchimp_sync/product_synchronizer.rb +32 -0
  27. data/app/synchronizers/solidus_mailchimp_sync/user_synchronizer.rb +44 -0
  28. data/app/synchronizers/solidus_mailchimp_sync/variant_synchronizer.rb +23 -0
  29. data/config/locales/en.yml +5 -0
  30. data/config/routes.rb +3 -0
  31. data/lib/generators/solidus_mailchimp_sync/install/install_generator.rb +27 -0
  32. data/lib/generators/solidus_mailchimp_sync/install/templates/config/initializers/solidus_mailchimp_sync.rb +36 -0
  33. data/lib/solidus_mailchimp_sync.rb +22 -0
  34. data/lib/solidus_mailchimp_sync/engine.rb +20 -0
  35. data/lib/solidus_mailchimp_sync/factories.rb +35 -0
  36. data/lib/solidus_mailchimp_sync/version.rb +3 -0
  37. data/lib/tasks/solidus_mailchimp_sync.rake +66 -0
  38. metadata +304 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 438a05bdc2da376a935c47c5e49a40c7fb051c29
4
+ data.tar.gz: fb84400208fe9076147845ebac7f07b976c48f6f
5
+ SHA512:
6
+ metadata.gz: 19cd7dd3771b03e505a7bee345a4e266d873e05c92db6ab859a32f2314ae7bd628cf0c019ccc16612e062b40f2d51f053d6321a024b0503e97ec45f769f73184
7
+ data.tar.gz: 8a677341ef4873597f3006a69cd92c24b96ba42e4320cbe431a4edf004ce26c4b4fec7ba15584a427648d0caaea9860cf6a8a3d769e48ba28470035567e6154a
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2016 Friends of the Web
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.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ [![Build Status](https://travis-ci.org/friendsoftheweb/solidus_mailchimp_sync.svg?branch=master)](https://travis-ci.org/friendsoftheweb/solidus_mailchimp_sync)
2
+
3
+ SolidusMailchimpSync
4
+ ====================
5
+
6
+ **WIP**
7
+
8
+ Synchronizes Solidus data with [Mailchimp E-Commerce API](http://developer.mailchimp.com/documentation/mailchimp/guides/getting-started-with-ecommerce/). (Mailchimp API 3.0)
9
+
10
+ This plugin does not do every kind of integration with Mailchimp that might be possible, it just focuses
11
+ on synchronizing data from Solidus to Mailchimp, by adding ActiveRecord lifecycle
12
+ hooks to Solidus models.
13
+
14
+ * Solidus `User` to Mailchimp `Customer`. (All customers currently added as status `transactional` in Mailchimp)
15
+ * Solidus `Product`, `Variant` (and images) to Mailchimp `Product` and `Variant`.
16
+ * Solidus `Order` (and their `LineItem`s), to Mailchimp `Cart` and `Order` (and their `Line`s).
17
+
18
+ Not all possible attributes that can be sync'd may yet be synced. It does
19
+ not yet sync Order payment/cancel/return/shipment state. It does not yet sync Customer
20
+ `orders_count`/`total_spent`. Some Mailchimp E-Commerce features may require these, others
21
+ are still usable.
22
+
23
+ Right now this plugin will connect everything in Solidus to a single Mailchimp `Store`, it does
24
+ not support multiple Mailchimp Stores.
25
+
26
+ We do not (yet?) support Mailchimp E-Commerce Link Tracking.
27
+
28
+ Actual sync'ing is done in background jobs using ActiveJob, configure your
29
+ ActiveJob adapter. All jobs are idempotent.
30
+
31
+ Installation
32
+ ------------
33
+
34
+ Add solidus_mailchimp_sync to your Gemfile:
35
+
36
+ ```ruby
37
+ gem 'solidus_mailchimp_sync'
38
+ ```
39
+
40
+ Bundle your dependencies and run the installation generator:
41
+
42
+ ```shell
43
+ bundle
44
+ bundle exec rails g solidus_mailchimp_sync:install
45
+ ```
46
+
47
+ Your app will need to set `default_url_options[:host]` so urls can be
48
+ sent to mailchimp in background. :
49
+
50
+ config.action_mailer.default_url_options = { host: 'mystore.example.org' }
51
+
52
+ Review the generated `./config/initializers/solidus_mailchimp_sync.rb` for required
53
+ and optional config, including mailchimp api keys.
54
+
55
+ You will need to have a Mailchimp List already created. You will need to create
56
+ a Mailchimp `Store` object belonging to that list, that we'll use for synchronization.
57
+ Not sure if that can be created through anything but API. You can create one by:
58
+
59
+ bundle exec rake solidus_mailchimp_sync:create_mailchimp_store LIST_ID=list-id-from-mailchimp
60
+
61
+ Then add the storeID for created store to your configuration.
62
+
63
+ Before going live, you will probably want to bulk-add all your existing data --
64
+ if existing products aren't added, orders won't be able to be synced. This
65
+ could take a while:
66
+
67
+ RAILS_ENV=production rake solidus_mailchimp_sync:bulk_sync
68
+
69
+ Known issues/To do
70
+ -------
71
+
72
+ * If a user changes their email addresses, their old orders may be no longer associated with
73
+ them in mailchimp, they will wind up with two mailchimp customer records. (Mailchimp
74
+ docs suggest you can change an existing Customer's id itself, but it didn't seem to work.
75
+ can't change an existing Customer's email address)
76
+ * Debounce: This may send a LOT of updates to mailchimp, when you're editing something.
77
+ In checkout process there are sometimes multiple syncs for order, not sure why.
78
+ Have an idea for an implementation debounce feature that could debounce/coalesce mailchimp
79
+ syncs in the general case.
80
+
81
+ Testing
82
+ -------
83
+
84
+ First bundle your dependencies, then run `rake`. `rake` will default to building the dummy app if it does not exist, then it will run specs, and [Rubocop](https://github.com/bbatsov/rubocop) static code analysis. The dummy app can be regenerated by using `rake test_app`.
85
+
86
+ ```shell
87
+ bundle
88
+ bundle exec rake
89
+ ```
90
+
91
+ Some tests use VCR to record live transactions with mailchimp. To run these tests while
92
+ recording new cassettes, you will need to set ENV `MAILCHIMP_API_KEY` and `MAILCHIMP_STORE_ID`.
93
+ **Note** these should be a test/dummy mailchimp account, as data will be edited by tests.
94
+
95
+ When testing your applications integration with this extension you may use it's factories.
96
+ (No factories at present)
97
+ Simply add this require statement to your spec_helper:
98
+
99
+ ```ruby
100
+ require 'solidus_mailchimp_sync/factories'
101
+ ```
102
+
103
+ Copyright (c) 2016 Friends of the Web LLC, released under the New BSD License
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ begin
6
+ require 'spree/testing_support/extension_rake'
7
+ require 'rubocop/rake_task'
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ RuboCop::RakeTask.new
13
+
14
+ #task default: %i(first_run rubocop spec)
15
+ task default: %i(first_run spec)
16
+ rescue LoadError
17
+ # no rspec available
18
+ end
19
+
20
+ task :first_run do
21
+ if Dir['spec/dummy'].empty?
22
+ Rake::Task[:test_app].invoke
23
+ Dir.chdir('../../')
24
+ end
25
+ end
26
+
27
+ desc 'Generates a dummy app for testing'
28
+ task :test_app do
29
+ ENV['LIB_NAME'] = 'solidus_mailchimp_sync'
30
+ Rake::Task['extension:test_app'].invoke
31
+ end
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js'
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js'
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
@@ -0,0 +1,15 @@
1
+ module SolidusMailchimpSync
2
+ class SyncJob < ActiveJob::Base
3
+ class_attribute :use_queue_name
4
+ use_queue_name = :default
5
+
6
+ queue_as do
7
+ self.use_queue_name
8
+ end
9
+
10
+ def perform(synchronizer_class_name, model)
11
+ synchronizer = synchronizer_class_name.constantize.new(model)
12
+ synchronizer.sync
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module SolidusMailchimpSync
2
+ class Error < StandardError
3
+ attr_reader :type, :title, :status, :detail, :instance,
4
+ :request_method, :request_url, :request_body,
5
+ :response_body, :response_hash
6
+
7
+ def initialize( type:nil, title:nil, status: nil, detail: nil, instance: nil,
8
+ request_method:nil, request_url: nil, request_body: nil,
9
+ response_body: nil, response_hash: nil)
10
+
11
+ @type = type
12
+ @title = title
13
+ @status = status
14
+ @detail = detail
15
+ @instance = instance
16
+
17
+ @request_method = request_method.to_s.upcase
18
+ @request_url = request_url
19
+ @request_body = request_body
20
+
21
+ @response_body = response_body
22
+ @response_hash = response_hash
23
+
24
+ super(constructed_message)
25
+ end
26
+
27
+ def constructed_message
28
+ errors = response_hash.try { |h| h["errors"] }.to_s.presence
29
+ [status, title, detail, errors].compact.join(': ')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ require 'json'
2
+ require 'http'
3
+ require 'solidus_mailchimp_sync'
4
+
5
+ module SolidusMailchimpSync
6
+ class Mailchimp
7
+ AUTH_USER = "ignored"
8
+
9
+ # If Mailchimp errors, will normally raise a SolidusMailchimpSync::Error, but
10
+ # set `return_errors: true` to return the Error as return value instead.
11
+ def self.request(method, path, body: nil, return_errors: false)
12
+ return unless SolidusMailchimpSync.enabled
13
+
14
+ if SolidusMailchimpSync.api_key.blank?
15
+ raise ArgumentError, "Missing required configuration `SolidusMailchimpSync.api_key`"
16
+ end
17
+
18
+ url = url(path)
19
+ args = [method.to_sym, url]
20
+ args << { json: body } if body
21
+ response = HTTP.basic_auth(user: AUTH_USER, pass: SolidusMailchimpSync.api_key).
22
+ request(*args)
23
+
24
+ response_hash = response.body.present? ? JSON.parse(response.body.to_s) : { status: response.code }
25
+
26
+ unless (200..299).cover?(response.code)
27
+ return Error.new(
28
+ request_method: method,
29
+ request_url: url,
30
+ request_body: body,
31
+
32
+ type: response_hash["type"],
33
+ title: response_hash["title"],
34
+ status: response_hash["status"] || response.code,
35
+ detail: response_hash["detail"],
36
+ instance: response_hash["instance"],
37
+
38
+ response_hash: response_hash
39
+ ).tap { |error| raise error unless return_errors }
40
+ end
41
+
42
+ response_hash
43
+ rescue JSON::ParserError => e
44
+ return Error.new(request_method: method, request_url: url, request_body: body,
45
+ status: response.status, detail: "JSON::ParserError #{e}",
46
+ response_body: response.body.to_s).tap { |error| raise error unless return_errors }
47
+ end
48
+
49
+ # Assumes an ECommerce request to our store, prefixes path argument with
50
+ # `/ecommerce/store/#{SolidusMailchimpSync.store_id}/`
51
+ def self.ecommerce_request(method, path, body: nil, store_id: SolidusMailchimpSync.store_id, return_errors: false)
52
+ if store_id.blank?
53
+ raise ArgumentError, "Missing required configuration `SolidusMailchimpSync.store_id`"
54
+ end
55
+
56
+ path = "/ecommerce/stores/#{store_id}/" + path.sub(%r{\A/}, '')
57
+ request(method, path, body: body, return_errors: return_errors)
58
+ end
59
+
60
+ def self.base_url
61
+ "https://#{SolidusMailchimpSync.data_center}.api.mailchimp.com/3.0/"
62
+ end
63
+
64
+ def self.url(path)
65
+ base_url + path.sub(%r{\A/}, '')
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+
3
+ module SolidusMailchimpSync
4
+ # ONLY includes email address. Everyone has their own user class,
5
+ # use a custom serializer to serialize other stuff. To change where
6
+ # email address is stored in user record, be sure to set
7
+ # SolidusMailchimpSync::UserSynchronizer.email_address_attribute
8
+ class CustomerSerializer
9
+ attr_reader :user
10
+
11
+ def initialize(user)
12
+ @user = user
13
+ unless user.persisted?
14
+ raise ArgumentError, "Can't serialize a non-saved user: #{user}"
15
+ end
16
+ end
17
+
18
+ def as_json
19
+ # Note mailchimp does not let us change email address, it won't be updated on
20
+ # subsequent pushes. So our mailchimp id includes the email address,
21
+ # and new mailchimp Customer will be created if email address changes.
22
+ {
23
+ 'id' => UserSynchronizer.customer_id(user),
24
+ 'email_address' => user.send(UserSynchronizer.email_address_attribute),
25
+ 'opt_in_status' => false
26
+ }
27
+ end
28
+
29
+ def to_json
30
+ JSON.dump(as_json)
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ module SolidusMailchimpSync
4
+ # Line item for Cart or Order, mailchimp serializes the same
5
+ class LineItemSerializer
6
+ attr_reader :line_item
7
+
8
+ def initialize(line_item)
9
+ @line_item = line_item
10
+ end
11
+
12
+ def as_json
13
+ {
14
+ id: line_item.id.to_s,
15
+ product_id: line_item.product.id.to_s,
16
+ product_variant_id: line_item.variant.id.to_s,
17
+ quantity: line_item.quantity,
18
+ price: line_item.price.to_f
19
+ }
20
+ end
21
+
22
+ def to_json
23
+ JSON.dump(as_json)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ require 'json'
2
+
3
+ module SolidusMailchimpSync
4
+ # Serializes to Mailchimp Cart or Order, depending on state.
5
+ class OrderSerializer
6
+ attr_reader :order
7
+
8
+ def initialize(order)
9
+ @order = order
10
+ unless order.persisted?
11
+ raise ArgumentError, "Can't serialize a non-saved order: #{order}"
12
+ end
13
+ end
14
+
15
+ def as_json
16
+ hash = {
17
+ id: order.id.to_s,
18
+ customer: {
19
+ id: UserSynchronizer.customer_id(order.user)
20
+ },
21
+ currency_code: order.currency,
22
+ order_total: order.total.to_f,
23
+ tax_total: order.tax_total.to_f,
24
+ lines: line_items
25
+ }
26
+
27
+
28
+ # Mailchimp does not take URLs for orders, just carts
29
+ unless order_complete?
30
+ url = cart_url
31
+ hash["checkout_url"] = url if url
32
+ end
33
+
34
+ hash["shipping_total"] = shipping_total if shipping_total
35
+
36
+ hash
37
+ end
38
+
39
+ # Override in custom serializer for custom front-end url
40
+ def cart_url
41
+ # Mailchimp does not take URLs for orders, just carts
42
+ unless order_complete?
43
+ if Rails.application.routes.default_url_options[:host] && Spree::Core::Engine.routes.url_helpers.respond_to?(:cart_url)
44
+ Spree::Core::Engine.routes.url_helpers.cart_url(host: Rails.application.routes.default_url_options[:host])
45
+ end
46
+ end
47
+ end
48
+
49
+ def order_complete?
50
+ # Yes, somehow solidus can sometimes, temporarily, in our after commit hook
51
+ # have state==complete set, but not completed_at
52
+ order.completed? || order.state == "complete"
53
+ end
54
+
55
+ def shipping_total
56
+ # Mailchimp only wants shipping total for Orders, not Carts.
57
+ order.shipment_total if order_complete?
58
+ end
59
+
60
+ def to_json
61
+ JSON.dump(as_json)
62
+ end
63
+
64
+ def line_items
65
+ order.line_items.collect do |line_item|
66
+ ::SolidusMailchimpSync::OrderSynchronizer.line_item_serializer_class_name.constantize.new(line_item).as_json
67
+ end
68
+ end
69
+ end
70
+ end