Skip to content
This repository has been archived by the owner on Apr 17, 2020. It is now read-only.

Commit

Permalink
Merge pull request DynamoMTL#76 from jkelleyj/feature/mailchimp-3.0
Browse files Browse the repository at this point in the history
Add support for Mailchimp v3.0 API via Gibbon
  • Loading branch information
braidn authored Nov 3, 2016
2 parents 9d33729 + 7e1d03d commit bc3dae9
Show file tree
Hide file tree
Showing 17 changed files with 886 additions and 246 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
Makes it easy to integrate your [Spree][1] app with [MailChimp][2].

**List synchronization**
> Automatically syncs Spree's user list with MailChimp. The user can subscribe/unsubscribe via the registration and account pages.
> Automatically syncs Spree's user list with MailChimp. The user can
> subscribe/unsubscribe via the registration and account pages.
**Order synchronoization**
> Fully supports MailChimp's [eCommerce360][3] API. Allows you to create targeted campaigns in MailChimp based on a user's purchase history. We'll even update MailChimp if the order changes after the sale (i.e. order modification, cancelation, return).
> Fully supports MailChimp's [eCommerce360][3] API. Allows you to
> create targeted campaigns in MailChimp based on a user's purchase history.
> We'll even update MailChimp if the order changes after the
> sale (i.e. order modification, cancelation, return). User's who check out
> with their email in the Spree Storefront, will accrue order data under this
> email in MailChimp. This data will be available under the 'E-Commerce' tab
> for the specific subscriber.
**Campaign Revenue Tracking**
> Notifies MailChimp when an order originates from a campaign email.
Expand All @@ -18,13 +25,19 @@ Makes it easy to integrate your [Spree][1] app with [MailChimp][2].
> Easily add your own custom merge vars. We'll only sync them when data changes.
**Existing Stores**
> Provides a handy rake task `rake spree_chimpy:orders:sync` is included to sync up all your existing order data with mail chimp. Run this after installing spree_chimpy to an existing store.
> Provides a handy rake task `rake spree_chimpy:orders:sync` is included
> to sync up all your existing order data with mail chimp. Run this after
> installing spree_chimpy to an existing store.
**Deferred Processing**
> Communication between Spree and MailChimp is synchronous by default. If you have `delayed_job` in your bundle, the communication is queued up and deferred to one of your workers. (`sidekiq` support also planned).
> Communication between Spree and MailChimp is synchronous by default. If you
> have `delayed_job` in your bundle, the communication is queued up and
> deferred to one of your workers. (`sidekiq` support also planned).
**Angular.js/Sprangular**
> You can integrate it with [sprangular](https://github.com/sprangular/sprangular) by using the [sprangular_chimpy](https://github.com/sprangular/sprangular_chimpy) gem.
> You can integrate it
> with [sprangular](https://github.com/sprangular/sprangular) by using
> the [sprangular_chimpy](https://github.com/sprangular/sprangular_chimpy) gem.
## Installing

Expand Down
62 changes: 62 additions & 0 deletions lib/spree/chimpy/interface/customer_upserter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Spree::Chimpy
module Interface
class CustomerUpserter
delegate :log, :store_api_call, to: Spree::Chimpy

def initialize(order)
@order = order
end
# CUSTOMER will be pulled first from the MC_EID if present on the order.source
# IF that is not found, customer will be found by our Customer ID
# IF that is not found, customer is created with the order email and our Customer ID
def ensure_customer
# use the one from mail chimp or fall back to the order's email
# happens when this is a new user
customer_id = customer_id_from_eid(@order.source.email_id) if @order.source
customer_id || upsert_customer
end

def self.mailchimp_customer_id(user_id)
"customer_#{user_id}"
end

def customer_id_from_eid(mc_eid)
email = Spree::Chimpy.list.email_for_id(mc_eid)
if email
begin
response = store_api_call
.customers
.retrieve(params: { "fields" => "customers.id", "email_address" => email })

data = response["customers"].first
data["id"] if data
rescue Gibbon::MailChimpError => e
nil
end
end
end

private

def upsert_customer
customer_id = self.class.mailchimp_customer_id(@order.user_id)
begin
response = store_api_call
.customers(customer_id)
.retrieve(params: { "fields" => "id,email_address"})
rescue Gibbon::MailChimpError => e
# Customer Not Found, so create them
response = store_api_call
.customers
.create(body: {
id: customer_id,
email_address: @order.email.downcase,
opt_in_status: Spree::Chimpy::Config.subscribe_to_list || false
})
end
customer_id
end

end
end
end
105 changes: 83 additions & 22 deletions lib/spree/chimpy/interface/list.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
require 'digest'

module Spree::Chimpy
module Interface
class List
delegate :log, to: Spree::Chimpy

def initialize(list_name, segment_name, double_opt_in, send_welcome_email, list_id)
@api = Spree::Chimpy.api
@list_id = list_id
@segment_name = segment_name
@double_opt_in = double_opt_in
@send_welcome_email = send_welcome_email
@list_name = list_name
end

def api_call
@api.lists
def api_call(list_id = nil)
if list_id
Spree::Chimpy.api.lists(list_id)
else
Spree::Chimpy.api.lists
end
end

def subscribe(email, merge_vars = {}, options = {})
log "Subscribing #{email} to #{@list_name}"

begin
api_call.subscribe(list_id, { email: email }, merge_vars, 'html', @double_opt_in, true, true, @send_welcome_email)
api_member_call(email)
.upsert(body: {
email_address: email,
status: "subscribed",
merge_fields: merge_vars,
email_type: 'html'
}) #, @double_opt_in, true, true, @send_welcome_email)

segment([email]) if options[:customer]
rescue Mailchimp::ListInvalidImportError, Mailchimp::ValidationError => ex
log "Subscriber #{email} rejected for reason: [#{ex.message}]"
rescue Gibbon::MailChimpError => ex
log "Subscriber #{email} rejected for reason: [#{ex.raw_body}]"
true
end
end
Expand All @@ -33,38 +44,72 @@ def unsubscribe(email)
log "Unsubscribing #{email} from #{@list_name}"

begin
api_call.unsubscribe(list_id, { email: email })
rescue Mailchimp::EmailNotExistsError, Mailchimp::ListNotSubscribedError
api_member_call(email)
.update(body: {
email_address: email,
status: "unsubscribed"
})
rescue Gibbon::MailChimpError => ex
log "Subscriber unsubscribe for #{email} failed for reason: [#{ex.raw_body}]"
true
end
end

def info(email_or_id)
log "Checking member info for #{email_or_id} from #{@list_name}"
def email_for_id(mc_eid)
log "Checking customer id for #{mc_eid} from #{@list_name}"
begin
response = api_list_call
.members
.retrieve(params: { "unique_email_id" => mc_eid, "fields" => "members.id,members.email_address" })

member_data = response["members"].first
member_data["email_address"] if member_data
rescue Gibbon::MailChimpError => ex
nil
end
end

def info(email)
log "Checking member info for #{email} from #{@list_name}"

#maximum of 50 emails allowed to be passed in
response = api_call.member_info(list_id, [{email: email_or_id}])
if response['success_count'] && response['success_count'] > 0
record = response['data'].first.symbolize_keys
begin
response = api_member_call(email)
.retrieve(params: { "fields" => "email_address,merge_fields,status"})

response = response.symbolize_keys
response.merge(email: response[:email_address])
rescue Gibbon::MailChimpError
{}
end

record.nil? ? {} : record
end

def merge_vars
log "Finding merge vars for #{@list_name}"

api_call.merge_vars([list_id])['data'].first['merge_vars'].map {|record| record['tag']}
response = api_list_call
.merge_fields
.retrieve(params: { "fields" => "merge_fields.tag,merge_fields.name"})
response["merge_fields"].map { |record| record['tag'] }
end

def add_merge_var(tag, description)
log "Adding merge var #{tag} to #{@list_name}"

api_call.merge_var_add(list_id, tag, description)
api_list_call
.merge_fields
.create(body: {
tag: tag,
name: description,
type: "text"
})
end

def find_list_id(name)
list = @api.lists.list["data"].detect { |r| r["name"] == name }
response = api_call
.retrieve(params: {"fields" => "lists.id,lists.name"})
list = response["lists"].detect { |r| r["name"] == name }
list["id"] if list
end

Expand All @@ -75,26 +120,42 @@ def list_id
def segment(emails = [])
log "Adding #{emails} to segment #{@segment_name} [#{segment_id}] in list [#{list_id}]"

params = emails.map { |email| { email: email } }
response = api_call.static_segment_members_add(list_id, segment_id.to_i, params)
api_list_call.segments(segment_id.to_i).create(body: { members_to_add: Array(emails) })
end

def create_segment
log "Creating segment #{@segment_name}"

@segment_id = api_call.static_segment_add(list_id, @segment_name)
result = api_list_call.segments.create(body: { name: @segment_name, static_segment: []})
@segment_id = result["id"]
end

def find_segment_id
segments = api_call.static_segments(list_id)
segment = segments.detect {|segment| segment['name'].downcase == @segment_name.downcase }
response = api_list_call
.segments
.retrieve(params: {"fields" => "segments.id,segments.name"})
segment = response["segments"].detect {|segment| segment['name'].downcase == @segment_name.downcase }

segment['id'] if segment
end

def segment_id
@segment_id ||= find_segment_id
end

def api_list_call
api_call(list_id)
end

def api_member_call(email)
api_list_call.members(email_to_lower_md5(email))
end

private

def email_to_lower_md5(email)
Digest::MD5.hexdigest(email.downcase)
end
end
end
end
87 changes: 87 additions & 0 deletions lib/spree/chimpy/interface/order_upserter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module Spree::Chimpy
module Interface
class OrderUpserter
delegate :log, :store_api_call, to: Spree::Chimpy

def initialize(order)
@order = order
end

def upsert
Products.ensure_products(@order)
perform_upsert
end

private

def perform_upsert
data = order_hash
log "Adding order #{@order.number} for #{data[:customer][:id]} with campaign #{data[:campaign_id]}"
begin
find_and_update_order(data)
rescue Gibbon::MailChimpError => e
log "Order #{@order.number} Not Found, creating order"
create_order(data)
end
end

def find_and_update_order(data)
# retrieval is checks if the order exists and raises a Gibbon::MailChimpError when not found
response = store_api_call.orders(@order.number).retrieve(params: { "fields" => "id" })
log "Order #{@order.number} exists, updating data"
store_api_call.orders(@order.number).update(body: data)
end

def create_order(data)
store_api_call
.orders
.create(body: data)
rescue Gibbon::MailChimpError => e
log "Unable to create order #{@order.number}. [#{e.raw_body}]"
end

def order_variant_hash(line_item)
variant = line_item.variant
{
id: "line_item_#{line_item.id}",
product_id: Products.mailchimp_product_id(variant),
product_variant_id: Products.mailchimp_variant_id(variant),
price: variant.price.to_f,
quantity: line_item.quantity
}
end

def order_hash
customer_id = CustomerUpserter.new(@order).ensure_customer
source = @order.source

lines = @order.line_items.map do |line|
# MC can only associate the order with a single category: associate the order with the category right below the root level taxon
order_variant_hash(line)
end

data = {
id: @order.number,
lines: lines,
order_total: @order.total.to_f,
financial_status: @order.payment_state || "",
fulfillment_status: @order.shipment_state || "",
currency_code: @order.currency,
processed_at_foreign: @order.completed_at ? @order.completed_at.to_formatted_s(:db) : "",
updated_at_foreign: @order.updated_at.to_formatted_s(:db),
shipping_total: @order.ship_total.to_f,
tax_total: @order.try(:included_tax_total).to_f + @order.try(:additional_tax_total).to_f,
customer: {
id: customer_id
}
}

if source
data[:campaign_id] = source.campaign_id
end

data
end
end
end
end
Loading

0 comments on commit bc3dae9

Please sign in to comment.