solidus_mailchimp_sync 1.0.0.beta01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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