SpreeGladly
[comment]: <> (add Travis status build badge when repo became public)
Overview
This exetension allows you to connect your Spree store with Gladly service. It allows Gladly agents to see basic information about Spree customers and their orders.
It adheres to the specification of a Gladly Lookup adapter as described here.
Supported Spree versions: 3.0
, 3.1
, 3.7
, 4.0
, 4.1
, 4.2
Table of contents
- SpreeGladly
Installation
Add this line to your application's Gemfile:
gem 'spree_gladly'
And then execute:
$ bundle install
Next, you should run the installer:
$ bundle exec rails generate spree_gladly:install
Configuration
Spree Store side
After installation, you will find in config/initializers/spree_gladly.rb
directory the below file:
SpreeGladly.setup do |config|
# The key used to validate the Gladly lookup request signature.
# We recommend using a secure random string of length over 32.
config.signing_key = '<%= SecureRandom.base64(32) %>'
# You can change serializer on your own
config.basic_lookup_presenter = Customer::BasicLookupPresenter
config.detailed_lookup_presenter = Customer::DetailedLookupPresenter
config.order_limit = nil
config.order_includes = [:line_items]
config.order_sorting = { created_at: :desc }
config.order_states = ['complete']
# The request's timestamp is validated against `signing_threshold` to prevent replay attacks.
# Setting this value to `0` disables the threshold validation.
# Default is `0`.
# config.signing_threshold = 5.minutes
end
where you are able to set the preferences:
- signing_key: cryptographic key to sign every request to your lookup service
- signing_threshold: time value to prevent replay attacks ( default: 0 )
- basic_lookup_presenter: presenter which is responsible for basic lookup
results
payload ( default: Customer::BasicLookupPresenter ) - detailed_lookup_presenter: presenter which is responsible for detailed lookup
results
payload ( default: Customer::DetailedLookupPresenter ) - order_limit: you can set limit returned orders number in
detailed lookup
response, ifnil
than no limitsdefault: nil
- order_includes: you can set what relation should be included in query,
default: :line_items
. This gets passed into .include() when fetching detailed lookup - if you want to display data from order's relationships, you may want to optimize the query - order_sorting: *you can set how returned orders should be sorted
default: { created_at: :desc }
- order_states: you can set order
state
which should be returned in responsedefault: ['complete']
. This defines states oforders
that will be returned to Gladly (and that by default it will excludeSpree::Orders
incart|address|delivery|payment
states
You can also set signing_key
and signing_threshold
via the admin dashboard in your Spree instance. To do that, open Gladly Settings
in the Configurations
section.
!!! Important !!!
Detailed lookups find customer's orders based on customer's profile, but will also include guest orders made with the same email address.
By default, Spree doesn't index the email
field of Spree::Orders
table. To ensure smooth operation of the lookup endpoint, add the following migration to your application.
class AddEmailIndexToSpreeOrders < ActiveRecord::Migration
def change
add_index :spree_orders, :email
end
end
Note: please adjust migration to yours Rails version
Gladly Service side:
Provide to your agent:
- lookup endpoint (
https://example-spree-store.com/api/v1/customers/lookup
), wherehttps://example-spree-store.com
is your Spree store URL. - signing_key
Events
Description
Within gem we introduce Conversations (Create Item) using API client.
We implemented two events against Spree::Order
model:
Placed
- it's fired up after Order is completed by customer (Gladly::Events::Order::Placed
)Refundned
- it's fired up after Order items are returned to customer (Gladly::Events::Order::Refunded
)
More about Conversations
Configuration
Important !!!
Without bellow settings events will won't work. You will get those from yours Gladly dashboard
config.gladly_api_username = '[email protected]'
config.gladly_api_key = 'api_key'
config.gladly_api_base_url = 'https://dev-example.gladly.qa'
config.turn_off_built_in_events = false
In case when you need write your own events class you can switch off built-in events by setting turn_off_built_in_events
as true
Customization
Within spree_gladly
gem we distinguish response for guest
and registerd
customer. For customize those, i.e detailed lookup response, to do that you have do following steps:
- replace
Customer::DetailedLookupPresenter
inconfig/initializers/spree_gladly.rb
initializer file with your own. - override methods
registerd_presenter
( default presenter ) orguest_presenter
( default presenter ) with your own.
Please consider below example:
class GladlyCustomersPresenter
include Spree::Core::Engine.routes.url_helpers # this is important if you want to use Spree routes
def initialize(resource:)
@resource = resource
end
def to_h
return [] unless resource.customer.present?
resource.guest ? guest_presenter : registered_presenter
end
private
attr_reader :resource
def registered_presenter
YourOwn::DetailedPresenter.new(resource: resource).to_h
end
def guest_presenter
Customer::Guest::DetailedPresenter.new(resource: resource).to_h
end
...
end
config/initializers/spree_gladly.rb
SpreeGladly.setup do |config|
# ...
config.basic_lookup_presenter = Customer::BasicLookupPresenter
config.detailed_lookup_presenter = GladlyCustomersPresenter
# ...
end
!!! Important !!!
If you would like to resign from to_h
or change initialize(resource:)
method, you have to override Spree::Api::V1::CustomersController#serialize_collection
to do that, please follow by this guide
To understand how to set up the integration in Gladly please refer to the Gladly help docs
You will need the following information:
- lookup endpoint (
https://example-spree-store.com/api/v1/customers/lookup
), wherehttps://example-spree-store.com
is your Spree store URL. - signing_key
Usage
To understand how the extension works from user's point of view please refer to the Gladly help docs.
The below description will assume that the reader has familiarized themselves with Gladly Lookup Adapter tutorial.
Basic Lookup
The aim of basic search is to allow the agent to find potential customers that match the information that they have at hand, e.g. email, phone number, name. As mentioned in the Gladly tutorial the initial search can be triggered automatically or by agent and because of that the Spree adapter accepts two different formats of requests.
Manual Search Request
OOTB the extension allows searching by email, phone number and name of the customer. Additional fields can be added.
{
"lookupLevel":"BASIC",
"uniqueMatchRequired":false,
"query":{
"emails":"[email protected]",
"phones":"666-666-666",
"name": "Elka Melka",
}
}
Automatic Search Request
The request is automatically populated with data available in customer's profile. OOTB the extension searches for customers based on the name and emails (ignoring the rest of the fields).
{
"lookupLevel":"BASIC",
"uniqueMatchRequired":false,
"query":
{
"emails":["[email protected]", "[email protected]"],
"phones": ["666-666-666", "123-435-235"],
"name": "Elka Melka",
"lifetimeValue": "$500"
}
}
Basic Lookup Response
By default the fields listed below are returned. Fields can be hidden via Gladly UI or the integration can be extended to return more/different fields (Customization). The format of the response has to match the Gladly Customer schema
Note: we use the email address as the externalCustomerId
and not the id used by Spree.
{
"results":[
{
"externalCustomerId": "[email protected]",
"customAttributes": {
"spreeId": 1
},
"name": "James Bond",
"emails": [{"original": "[email protected]"}],
"phones": [{"original": "666-666-666"}],
"address": "22 Apple Lane, 9999 San Francisco"
}
]
}
Detailed Lookup
Detailed lookup is used to update the linked customer with detailed data. This means that detailed lookup expects only one result returned. Gladly will send in the request all the customer's information except transactions. However, only the externalCustomerId
is used to find the correct customer in Spree.
request payload:
{
"lookupLevel":"DETAILED",
"uniqueMatchRequired":true,
"query":{
"name": "Some name",
"totalOrderCount": "4",
"emails":[ "[email protected]", "[email protected]"],
"phones":[
"666-666-666"
],
"externalCustomerId":"[email protected]",
"customAttributes": {
"spreeId": 1
}
}
}
response payload: The following payload shows all the fields that are by default returned from Spree. You can ask your Gladly representative to amend which fields are visible in the order card or the integration can be extended to return more/different fields (Customization)
{
"results":[
{
"externalCustomerId": "[email protected]",
"name":"James Bond",
"address":"Baker Street 007 London, AK 00021 United Kingdom",
"emails":[
{
"original":"[email protected]"
}
],
"phones":[
{
"original":"666-666-666"
}
],
"customAttributes":{
"spreeId": "4",
"lifetimeValue":"$142.97",
"totalOrderCount":"2",
"guestOrderCount": "0",
"memberSince" : "May 17, 2021 10:18 AM UTC",
"customerLink": "https://example-spree-store.com/admin/users/4/edit"
},
"transactions":[
{
"type":"ORDER",
"orderStatus":"complete",
"orderNumber":"R185194841",
"orderLink":"https://example-spree-store.com/admin/orders/R185194841/edit",
"note":"",
"orderTotal":"$83.99",
"createdAt":"2021-06-21T10:29:43.881Z",
"products":[
{
"name":"Flared Midi Skirt",
"status":"fulfilled",
"sku": "SKU-1",
"quantity":"1",
"unitPrice":"$78.99",
"total": "$78.99",
"imageUrl":""
}
]
},
{
"type":"ORDER",
"orderStatus":"complete",
"orderNumber":"R461455233",
"orderLink":"https://example-spree-store.com/admin/orders/R461455233/edit",
"note":"Some note about the order",
"orderTotal":"$58.98",
"createdAt":"2021-06-21T10:34:10.262Z",
"products":[
{
"name":"3 4 Sleeve T Shirt",
"status":"fulfilled",
"sku": "SKU-2",
"quantity":"2",
"unitPrice":"$26.99",
"total": "$53.98",
"imageUrl":"https://example-spree-store.com/images/sample_picture.jpg"
}
]
}
]
}
]
}
How does the search work? What do the fields mean?
It's worth noting that in Spree customers are able to create orders without being logged in. It is important to be able to link such customers with their Gladly profiles to be able to help them.
This slightly complicates the search and makes it worth an explanation. Since we want to be able to identify both registered customers and customers with only guest orders, we are using the email address instead of the Spree::Account.id
as externalCustomerId
in Gladly (the unique identifier of a customer in an external system).
Note details on the exact format of the jsons was shown in previous section. The below tables are for information of the meaning of the fields in Spree.
Basic search
As mentioned in Basic Lookup searching by name, emails and phone numbers is possible. To simplify the search logic and improve performance, searching by name and phone number will only search customers with Spree profiles. Searching by email searches both registered customers and guest orders. It happens in two phases (for each customer's email separately):
- Attempt to find any customers with a Spree profile with the given email. If found, search will return that customer.
- If no customer is found the search will attempt to find all orders associated with the given email. If multiple orders are found it will return the details of the latest (
completed_at
) order.
Below table explains the returned fields.
For registered customers:
Gladly customer field | Spree field |
---|---|
name | Spree::User.bill_address.full_name |
externalCustomerId | Spree::User.email |
emails | Spree::User.email |
customAttributes.spreeId | Spree::User.id |
phones | Spree::User.bill_address.phone |
address | Spree::User.bill_address (address1, address2, city, zipcode) |
For guest customers
In the below table Spree::Order means the latest (Spree::Order.completed_at
) order that matched the email from the search.
Gladly field | Spree |
---|---|
name | Spree::Order.bill_address.full_name |
externalCustomerId | Spree::Order.email |
emails | Spree::Order.email |
customAttributes.spreeId | - |
phones | Spree:Order.bill_address.phone |
address | Spree::Order.bill_address.(address1, address2, city, zipcode) |
Detailed search
Detailed search assumes that we have found the right customer and we know their unique identifier. In Gladly this is the externalCustomerId
and in Spree, it's equivalent to the email address.
The below tables list the fields returned from Spree.
For registered customers:
Gladly field | Spree field |
---|---|
name | Spree::User.bill_address.full_name |
externalCustomerId | Spree::User.email |
emails | Spree::User.email |
phones | Spree::User.bill_address.phone |
address | Spree::User.bill_address.(address1 , address2, city, zipcode) |
customAttributes.spreeId | Spree::User.id |
customAttributes.totalOrderCount | total of Spree::Order(s) that match the externalCustomerId |
customAttributes.guestOrderCount | customAttributes.totalOrderCount - Spree::Account.attributes.completed_orders |
customAttributes.memberSince | Spree::User.created_at |
customAttributes.customerLink | customer_profile_url(Spree::User) |
customAttributes.lifetimeValue | sum total field of Spree::Order(s) that match the externalCustomerId |
transactions | details of all Spree::Orders that match the externalCustomerId |
For guest customers
Gladly field | Spree field |
---|---|
name | - |
externalCustomerId | Spree::User.email |
emails | Spree::User.email |
phones | - |
address | - |
customAttributes.spreeId | - |
customAttributes.totalOrderCount | total of Spree::Orders that match the externalCustomerId |
customAttributes.guestOrderCount | customAttributes.totalOrderCount |
customAttributes.memberSince | - |
customAttributes.customerLink | - |
customAttributes.lifetimeValue | sum total field of Spree::Order(s) that match the externalCustomerId |
transactions | details of all Spree::Orders that match the externalCustomerId |
Customization
Within spree_gladly
gem we distinguish response for guest
and registerd
customer. For customize those, i.e detailed lookup response, to do that you have do following steps:
- replace
Customer::DetailedLookupPresenter
inconfig/initializers/spree_gladly.rb
initializer file with your own. - override methods
registerd_presenter
( default presenter ) orguest_presenter
( default presenter ) with your own.
Please consider below example:
class GladlyCustomersPresenter
include Spree::Core::Engine.routes.url_helpers # this is important if you want to use Spree routes
def initialize(resource:)
@resource = resource
end
def to_h
return [] unless resource.customer.present?
resource.guest ? guest_presenter : registered_presenter
end
private
attr_reader :resource
def registered_presenter
YourOwn::DetailedPresenter.new(resource: resource).to_h
end
def guest_presenter
Customer::Guest::DetailedPresenter.new(resource: resource).to_h
end
...
end
config/initializers/spree_gladly.rb
SpreeGladly.setup do |config|
# ...
config.basic_lookup_presenter = Customer::BasicLookupPresenter
config.detailed_lookup_presenter = GladlyCustomersPresenter
# ...
end
!!! Important !!!
If you would like to resign from to_h
or change initialize(resource:)
method, you have to override Spree::Api::V1::CustomersController#serialize_collection
to do that, please follow by this guide
app/controllers/spree/api/v1/customers_controller.rb
def serialize_collection(type:, collection:)
presenter = {
detailed: SpreeGladly::Config.detailed_lookup_presenter.new(resource: collection),
basic: SpreeGladly::Config.basic_lookup_presenter.new(resource: collection)
}[type]
{ results: presenter.to_h }
end
Setup sandbox environment
- Deploy Spree store on hosting provider ( heroku example )
- Install
spree_gladly
gem - Setup
signing_key
and provide to Gladly agent
Testing
bundle exec rake test_app
bundle exec rake spec
To run specific test:
bundle exec rspec spec/controllers/spree/api/v1/customers_controller_spec.rb
Testing against specific Spree version:
export BUNDLE_GEMFILE=gemfiles/spree_3_7.gemfile
bundle install
bundle exec rake test_app
bundle exec rake spec
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/upsidelab/spree_gladly.
Code of Conduct
Everyone interacting in the SpreeGladly project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.