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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +103 -0
- data/Rakefile +31 -0
- data/app/assets/javascripts/spree/backend/solidus_mailchimp_sync.js +2 -0
- data/app/assets/javascripts/spree/frontend/solidus_mailchimp_sync.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_mailchimp_sync.css +4 -0
- data/app/assets/stylesheets/spree/frontend/solidus_mailchimp_sync.css +4 -0
- data/app/jobs/solidus_mailchimp_sync/sync_job.rb +15 -0
- data/app/models/solidus_mailchimp_sync/error.rb +32 -0
- data/app/models/solidus_mailchimp_sync/mailchimp.rb +69 -0
- data/app/serializers/solidus_mailchimp_sync/customer_serializer.rb +34 -0
- data/app/serializers/solidus_mailchimp_sync/line_item_serializer.rb +26 -0
- data/app/serializers/solidus_mailchimp_sync/order_serializer.rb +70 -0
- data/app/serializers/solidus_mailchimp_sync/product_serializer.rb +44 -0
- data/app/serializers/solidus_mailchimp_sync/variant_serializer.rb +59 -0
- data/app/solidus_decorators/spree/image_decorator.rb +16 -0
- data/app/solidus_decorators/spree/line_item_decorator.rb +8 -0
- data/app/solidus_decorators/spree/order_decorator.rb +8 -0
- data/app/solidus_decorators/spree/price_decorator.rb +10 -0
- data/app/solidus_decorators/spree/product_decorator.rb +8 -0
- data/app/solidus_decorators/spree/user_decorator.rb +10 -0
- data/app/solidus_decorators/spree/variant_decorator.rb +8 -0
- data/app/synchronizers/solidus_mailchimp_sync/base_synchronizer.rb +74 -0
- data/app/synchronizers/solidus_mailchimp_sync/order_synchronizer.rb +108 -0
- data/app/synchronizers/solidus_mailchimp_sync/product_synchronizer.rb +32 -0
- data/app/synchronizers/solidus_mailchimp_sync/user_synchronizer.rb +44 -0
- data/app/synchronizers/solidus_mailchimp_sync/variant_synchronizer.rb +23 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/solidus_mailchimp_sync/install/install_generator.rb +27 -0
- data/lib/generators/solidus_mailchimp_sync/install/templates/config/initializers/solidus_mailchimp_sync.rb +36 -0
- data/lib/solidus_mailchimp_sync.rb +22 -0
- data/lib/solidus_mailchimp_sync/engine.rb +20 -0
- data/lib/solidus_mailchimp_sync/factories.rb +35 -0
- data/lib/solidus_mailchimp_sync/version.rb +3 -0
- data/lib/tasks/solidus_mailchimp_sync.rake +66 -0
- 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
|
+
[](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,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
|