solidus_signifyd 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +21 -0
- data/Gemfile +10 -3
- data/README.md +58 -1
- data/app/controllers/spree/api/spree_signifyd/orders_controller.rb +1 -1
- data/app/models/spree/signifyd_configuration.rb +1 -0
- data/app/models/spree_signifyd/order_concerns.rb +18 -1
- data/app/models/spree_signifyd/shipment_decorator.rb +1 -1
- data/app/serializers/spree_signifyd/credit_card_serializer.rb +9 -9
- data/app/serializers/spree_signifyd/order_serializer.rb +22 -10
- data/app/serializers/spree_signifyd/user_serializer.rb +13 -2
- data/lib/spree_signifyd.rb +2 -3
- data/lib/spree_signifyd/create_signifyd_case.rb +3 -4
- data/lib/spree_signifyd/engine.rb +1 -0
- data/solidus_signifyd.gemspec +4 -4
- data/spec/controllers/spree/api/spree_signifyd/orders_controller_spec.rb +6 -10
- data/spec/lib/spree_signifyd/create_signifyd_case_spec.rb +4 -4
- data/spec/lib/spree_signifyd_spec.rb +14 -3
- data/spec/models/spree/order_spec.rb +38 -11
- data/spec/models/spree/shipment_spec.rb +33 -35
- data/spec/serializers/spree_signifyd/billing_address_serializer.rb +1 -1
- data/spec/serializers/spree_signifyd/credit_card_serializer_spec.rb +2 -2
- data/spec/serializers/spree_signifyd/delivery_address_serializer_spec.rb +2 -2
- data/spec/serializers/spree_signifyd/order_serializer_spec.rb +58 -17
- data/spec/serializers/spree_signifyd/user_serializer_spec.rb +9 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/support/api_schema_matcher.rb +9 -0
- data/spec/support/schemas/v2/case.json +305 -0
- metadata +46 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c7e1bb9ec394555d95c0bd04879ed53f60f36a4
|
4
|
+
data.tar.gz: 1ea89e5ec06118d867ba7fd1582785b64caaf129
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 601b96f317fb0f2b439f4550c910c1bcbfafe6cd0e1419f3a651c2aa2b7cb9ae956fa717b1abcc828628f11a63c45f976d7c84cf682b5f764fe1fe0772974885
|
7
|
+
data.tar.gz: fb0afbf0e113611b9f45e9e0011909acfa441590681910aead26d0a01784e646bb7752b71022bdfe57c96147095bf1c2775368f319a0ce5aa75388b989495804
|
data/.travis.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
sudo: false
|
2
|
+
cache: bundler
|
3
|
+
language: ruby
|
4
|
+
rvm:
|
5
|
+
- 2.3.1
|
6
|
+
env:
|
7
|
+
matrix:
|
8
|
+
- SOLIDUS_BRANCH=v1.0 DB=postgres
|
9
|
+
- SOLIDUS_BRANCH=v1.1 DB=postgres
|
10
|
+
- SOLIDUS_BRANCH=v1.2 DB=postgres
|
11
|
+
- SOLIDUS_BRANCH=v1.3 DB=postgres
|
12
|
+
- SOLIDUS_BRANCH=v1.4 DB=postgres
|
13
|
+
- SOLIDUS_BRANCH=v2.0 DB=postgres
|
14
|
+
- SOLIDUS_BRANCH=master DB=postgres
|
15
|
+
- SOLIDUS_BRANCH=v1.0 DB=mysql
|
16
|
+
- SOLIDUS_BRANCH=v1.1 DB=mysql
|
17
|
+
- SOLIDUS_BRANCH=v1.2 DB=mysql
|
18
|
+
- SOLIDUS_BRANCH=v1.3 DB=mysql
|
19
|
+
- SOLIDUS_BRANCH=v1.4 DB=mysql
|
20
|
+
- SOLIDUS_BRANCH=v2.0 DB=mysql
|
21
|
+
- SOLIDUS_BRANCH=master DB=mysql
|
data/Gemfile
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
|
-
|
4
|
-
gem "
|
3
|
+
branch = ENV.fetch('SOLIDUS_BRANCH', 'master')
|
4
|
+
gem "solidus", github: "solidusio/solidus", branch: branch
|
5
|
+
|
6
|
+
if branch == 'master' || branch >= "v2.0"
|
7
|
+
gem "rails-controller-testing", group: :test
|
8
|
+
end
|
9
|
+
|
10
|
+
gem 'pg'
|
11
|
+
gem 'mysql2'
|
5
12
|
|
6
13
|
group :development, :test do
|
7
14
|
gem "pry-rails"
|
data/README.md
CHANGED
@@ -4,7 +4,15 @@ Solidus Signifyd
|
|
4
4
|
Integration with Signifyd that implements a fraud check prior to marking a
|
5
5
|
shipment as ready to be shipped.
|
6
6
|
|
7
|
-
[![
|
7
|
+
[![Build Status](https://travis-ci.org/solidusio/solidus_signifyd.svg?branch=master)](https://travis-ci.org/solidusio/solidus_signifyd)
|
8
|
+
|
9
|
+
* All orders are sent to SIGNIFYD for scoring when they transition to complete.
|
10
|
+
* Risk analysis is returned from SIGNIFYD via a webhook and added to order.
|
11
|
+
* Orders with a risk score >= 500 (default review disposition threshhold)
|
12
|
+
- Paid orders are marked ready to ship.
|
13
|
+
* Orders with a risk score < 500
|
14
|
+
- Are cancelled.
|
15
|
+
- Risk analysis is displayed in admin.
|
8
16
|
|
9
17
|
Installation
|
10
18
|
------------
|
@@ -22,6 +30,53 @@ bundle
|
|
22
30
|
bundle exec rails g solidus_signifyd:install
|
23
31
|
```
|
24
32
|
|
33
|
+
Create a SIGNIFYD test team within the SIGNIFYD account. The API key is listed on the Teams page after a team has been created.
|
34
|
+
|
35
|
+
Create SIGNIFYD notifications for each event type and provide your
|
36
|
+
`api_spree_signifyd_orders_path`. To work with external webhook in local
|
37
|
+
development you may need to change the rails server [default host] and enable
|
38
|
+
port forwarding or setup a reverse SSH tunnel.
|
39
|
+
|
40
|
+
```
|
41
|
+
http://www.example.com/api/spree_signifyd/orders
|
42
|
+
```
|
43
|
+
|
44
|
+
Cases can be inspected in the SIGNIFYD web console.
|
45
|
+
|
46
|
+
Configuration
|
47
|
+
-------------
|
48
|
+
|
49
|
+
### api_key
|
50
|
+
|
51
|
+
Type: `string`
|
52
|
+
|
53
|
+
SIGNIFYD team API key.
|
54
|
+
|
55
|
+
### exclude_store_credit_orders
|
56
|
+
|
57
|
+
Type: `boolean`
|
58
|
+
Default: `false`
|
59
|
+
|
60
|
+
By default, even orders which are fully paid with store credit are sent to
|
61
|
+
SIGNIFYD. Since this could result in unnecessary charges to a user who is on a
|
62
|
+
"flat rate" plan, we provide the option to skip these orders.
|
63
|
+
|
64
|
+
### signifyd_score_threshold
|
65
|
+
|
66
|
+
Type: `integer`
|
67
|
+
Default: `500`
|
68
|
+
|
69
|
+
Automatic approval is granted to orders which have a good "reviewDisposition" or
|
70
|
+
have a score greater than the `signifyd_score_threshold`.
|
71
|
+
|
72
|
+
Risky Orders
|
73
|
+
------------
|
74
|
+
|
75
|
+
Flagging a case as bad in the SIGNIFYD web console will associate
|
76
|
+
a fraudulent case with the order's email. This will cause future orders to drop
|
77
|
+
below the `reviewDisposition` threshhold of 500 and allow you to inspect a
|
78
|
+
risky order.
|
79
|
+
|
25
80
|
Testing
|
26
81
|
-------
|
27
82
|
|
@@ -32,3 +87,5 @@ app can be regenerated by using `rake test_app`.
|
|
32
87
|
```shell
|
33
88
|
bundle exec rake
|
34
89
|
```
|
90
|
+
|
91
|
+
[default host]: http://guides.rubyonrails.org/4_2_release_notes.html#default-host-for-rails-server
|
@@ -21,7 +21,7 @@ module Spree::Api::SpreeSignifyd
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def authorize
|
24
|
-
request_sha = request.headers['
|
24
|
+
request_sha = request.headers['HTTP_X_SIGNIFYD_SEC_HMAC_SHA256']
|
25
25
|
computed_sha = build_sha(SpreeSignifyd::Config[:api_key], request.raw_post)
|
26
26
|
|
27
27
|
if !Devise.secure_compare(request_sha, computed_sha)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Spree
|
2
2
|
class SignifydConfiguration < Preferences::Configuration
|
3
3
|
preference :api_key, :string
|
4
|
+
preference :exclude_store_credit_orders, :boolean, default: false
|
4
5
|
preference :signifyd_score_threshold, :integer, default: 500 # Signifyd's recommended threshold
|
5
6
|
end
|
6
7
|
end
|
@@ -3,7 +3,11 @@ module SpreeSignifyd::OrderConcerns
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
Spree::Order.state_machine.after_transition to: :complete, unless: :approved? do |order, transition|
|
6
|
-
|
6
|
+
if order.send_to_signifyd?
|
7
|
+
SpreeSignifyd.create_case(order_number: order.number)
|
8
|
+
else
|
9
|
+
SpreeSignifyd.approve(order: order)
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
has_one :signifyd_order_score, class_name: "SpreeSignifyd::OrderScore"
|
@@ -19,5 +23,18 @@ module SpreeSignifyd::OrderConcerns
|
|
19
23
|
def awaiting_approval?
|
20
24
|
!signifyd_order_score
|
21
25
|
end
|
26
|
+
|
27
|
+
def send_to_signifyd?
|
28
|
+
!approved? &&
|
29
|
+
!(SpreeSignifyd::Config[:exclude_store_credit_orders] && paid_completely_with_store_credit?)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def paid_completely_with_store_credit?
|
35
|
+
payments.all? do |payment|
|
36
|
+
payment.payment_method.is_a?(Spree::PaymentMethod::StoreCredit)
|
37
|
+
end
|
38
|
+
end
|
22
39
|
end
|
23
40
|
end
|
@@ -4,7 +4,15 @@ module SpreeSignifyd
|
|
4
4
|
class CreditCardSerializer < ActiveModel::Serializer
|
5
5
|
self.root = false
|
6
6
|
|
7
|
-
attributes :cardHolderName, :last4
|
7
|
+
attributes :cardHolderName, :last4
|
8
|
+
|
9
|
+
# this is how to conditionally include attributes in AMS
|
10
|
+
def attributes(*args)
|
11
|
+
hash = super
|
12
|
+
hash[:expiryMonth] = object.month.to_i if object.month
|
13
|
+
hash[:expiryYear] = object.year.to_i if object.year
|
14
|
+
hash
|
15
|
+
end
|
8
16
|
|
9
17
|
def cardHolderName
|
10
18
|
"#{object.first_name} #{object.last_name}"
|
@@ -13,13 +21,5 @@ module SpreeSignifyd
|
|
13
21
|
def last4
|
14
22
|
object.last_digits
|
15
23
|
end
|
16
|
-
|
17
|
-
def expiryMonth
|
18
|
-
object.month
|
19
|
-
end
|
20
|
-
|
21
|
-
def expiryYear
|
22
|
-
object.year
|
23
|
-
end
|
24
24
|
end
|
25
25
|
end
|
@@ -8,16 +8,11 @@ module SpreeSignifyd
|
|
8
8
|
has_one :user, serializer: SpreeSignifyd::UserSerializer, root: "userAccount"
|
9
9
|
|
10
10
|
def purchase
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
'totalPrice' => object.total,
|
17
|
-
'products' => products,
|
18
|
-
'avsResponseCode' => latest_payment.try(:avs_response),
|
19
|
-
'cvvResponseCode' => latest_payment.try(:cvv_response_code)
|
20
|
-
}
|
11
|
+
build_purchase_information.tap do |purchase_info|
|
12
|
+
if paid_by_paypal?
|
13
|
+
purchase_info["paymentGateway"] = "paypal_account"
|
14
|
+
end
|
15
|
+
end
|
21
16
|
end
|
22
17
|
|
23
18
|
def recipient
|
@@ -41,6 +36,23 @@ module SpreeSignifyd
|
|
41
36
|
|
42
37
|
private
|
43
38
|
|
39
|
+
def paid_by_paypal?
|
40
|
+
latest_payment.try!(:source).try(:cc_type) == "paypal"
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_purchase_information
|
44
|
+
{
|
45
|
+
'browserIpAddress' => object.last_ip_address || "",
|
46
|
+
'orderId' => object.number,
|
47
|
+
'createdAt' => object.completed_at.utc.iso8601,
|
48
|
+
'currency' => object.currency,
|
49
|
+
'totalPrice' => object.total.to_f,
|
50
|
+
'products' => products,
|
51
|
+
'avsResponseCode' => latest_payment.try!(:avs_response) || "",
|
52
|
+
'cvvResponseCode' => latest_payment.try!(:cvv_response_code) || ""
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
44
56
|
def products
|
45
57
|
order_products = []
|
46
58
|
|
@@ -4,7 +4,14 @@ module SpreeSignifyd
|
|
4
4
|
class UserSerializer < ActiveModel::Serializer
|
5
5
|
self.root = false
|
6
6
|
|
7
|
-
attributes :emailAddress, :username, :createdDate, :lastUpdateDate, :
|
7
|
+
attributes :emailAddress, :username, :createdDate, :lastUpdateDate, :aggregateOrderCount, :aggregateOrderDollars, :phone
|
8
|
+
|
9
|
+
# this is how to conditionally include attributes in AMS
|
10
|
+
def attributes(*args)
|
11
|
+
hash = super
|
12
|
+
hash[:lastOrderId] = lastOrderId if lastOrderId.present?
|
13
|
+
hash
|
14
|
+
end
|
8
15
|
|
9
16
|
def emailAddress
|
10
17
|
object.email
|
@@ -31,7 +38,11 @@ module SpreeSignifyd
|
|
31
38
|
end
|
32
39
|
|
33
40
|
def aggregateOrderDollars
|
34
|
-
completed_orders.sum(:total)
|
41
|
+
completed_orders.sum(:total).to_f
|
42
|
+
end
|
43
|
+
|
44
|
+
def phone
|
45
|
+
object.orders.order("created_at DESC").first.try!(:ship_address).try!(:phone)
|
35
46
|
end
|
36
47
|
|
37
48
|
private
|
data/lib/spree_signifyd.rb
CHANGED
@@ -3,7 +3,6 @@ require 'signifyd'
|
|
3
3
|
require 'spree_signifyd/create_signifyd_case'
|
4
4
|
require 'spree_signifyd/engine'
|
5
5
|
require 'spree_signifyd/request_verifier'
|
6
|
-
require 'resque'
|
7
6
|
require 'devise'
|
8
7
|
|
9
8
|
module SpreeSignifyd
|
@@ -20,14 +19,14 @@ module SpreeSignifyd
|
|
20
19
|
|
21
20
|
def approve(order:)
|
22
21
|
order.contents.approve(name: self.name)
|
23
|
-
order.shipments.each { |shipment| shipment.ready! if shipment.
|
22
|
+
order.shipments.each { |shipment| shipment.ready! if shipment.can_ready? }
|
24
23
|
order.updater.update_shipment_state
|
25
24
|
order.save!
|
26
25
|
end
|
27
26
|
|
28
27
|
def create_case(order_number:)
|
29
28
|
Rails.logger.info "Queuing Signifyd case creation event: #{order_number}"
|
30
|
-
|
29
|
+
SpreeSignifyd::CreateSignifydCase.perform_later(order_number)
|
31
30
|
end
|
32
31
|
|
33
32
|
def score_above_threshold?(score)
|
@@ -1,13 +1,12 @@
|
|
1
1
|
module SpreeSignifyd
|
2
|
-
class CreateSignifydCase
|
3
|
-
|
2
|
+
class CreateSignifydCase < ActiveJob::Base
|
3
|
+
queue_as :default
|
4
4
|
|
5
|
-
def
|
5
|
+
def perform(order_number_or_id)
|
6
6
|
Rails.logger.info "Processing Signifyd case creation event: #{order_number_or_id}"
|
7
7
|
order = Spree::Order.find_by(number: order_number_or_id) || Spree::Order.find(order_number_or_id)
|
8
8
|
order_data = JSON.parse(OrderSerializer.new(order).to_json)
|
9
9
|
Signifyd::Case.create(order_data, SpreeSignifyd::Config[:api_key])
|
10
10
|
end
|
11
|
-
|
12
11
|
end
|
13
12
|
end
|
data/solidus_signifyd.gemspec
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.name = "solidus_signifyd"
|
6
|
-
s.version = "1.0
|
6
|
+
s.version = "1.1.0"
|
7
7
|
s.summary = "Solidus extension for communicating with Signifyd to check orders for fraud."
|
8
8
|
s.description = s.summary
|
9
9
|
|
@@ -20,12 +20,12 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.requirements << "none"
|
21
21
|
|
22
22
|
s.add_dependency "active_model_serializers", "0.9.3"
|
23
|
-
s.add_dependency "resque", "~> 1.25.1"
|
24
23
|
s.add_dependency "signifyd", "~> 0.1.5"
|
25
|
-
s.add_dependency "solidus", "
|
24
|
+
s.add_dependency "solidus", [">= 1.0", "< 3"]
|
26
25
|
s.add_dependency "devise"
|
27
26
|
|
28
|
-
s.add_development_dependency "rspec-rails", "~>
|
27
|
+
s.add_development_dependency "rspec-rails", "~> 3.4"
|
28
|
+
s.add_development_dependency "json-schema"
|
29
29
|
s.add_development_dependency "simplecov"
|
30
30
|
s.add_development_dependency "sqlite3"
|
31
31
|
s.add_development_dependency "sass-rails"
|
@@ -38,13 +38,9 @@ module Spree::Api::SpreeSignifyd
|
|
38
38
|
}
|
39
39
|
}
|
40
40
|
|
41
|
-
before
|
42
|
-
|
43
|
-
around do |example|
|
44
|
-
previous_api_key = SpreeSignifyd::Config[:api_key]
|
41
|
+
before do
|
42
|
+
request.headers['HTTP_X_SIGNIFYD_SEC_HMAC_SHA256'] = signifyd_sha
|
45
43
|
SpreeSignifyd::Config[:api_key] = 'ABCDE'
|
46
|
-
example.run
|
47
|
-
SpreeSignifyd::Config[:api_key] = previous_api_key
|
48
44
|
end
|
49
45
|
|
50
46
|
routes { Spree::Core::Engine.routes }
|
@@ -79,9 +75,9 @@ module Spree::Api::SpreeSignifyd
|
|
79
75
|
context "the order has been shipped" do
|
80
76
|
|
81
77
|
it "returns without trying to act on the order" do
|
82
|
-
Spree::Order.
|
78
|
+
allow_any_instance_of(Spree::Order).to receive(:shipped?).and_return(true)
|
83
79
|
expect(SpreeSignifyd).not_to receive(:approve)
|
84
|
-
|
80
|
+
expect_any_instance_of(Spree::Order).not_to receive(:cancel!)
|
85
81
|
expect { subject }.not_to raise_error
|
86
82
|
expect(response.status).to eq(200)
|
87
83
|
end
|
@@ -92,7 +88,7 @@ module Spree::Api::SpreeSignifyd
|
|
92
88
|
|
93
89
|
it "returns without trying to act on the order" do
|
94
90
|
expect(SpreeSignifyd).not_to receive(:approve)
|
95
|
-
|
91
|
+
expect_any_instance_of(Spree::Order).not_to receive(:cancel!)
|
96
92
|
expect { subject }.not_to raise_error
|
97
93
|
expect(response.status).to eq(200)
|
98
94
|
end
|
@@ -121,7 +117,7 @@ module Spree::Api::SpreeSignifyd
|
|
121
117
|
after(:each) { body['reviewDiposition'] = @original_review_disposition }
|
122
118
|
|
123
119
|
it 'cancels the order' do
|
124
|
-
Spree::Order.
|
120
|
+
expect_any_instance_of(Spree::Order).to receive(:cancel!)
|
125
121
|
subject
|
126
122
|
end
|
127
123
|
end
|
@@ -7,13 +7,13 @@ module SpreeSignifyd
|
|
7
7
|
let(:json) { JSON.parse(OrderSerializer.new(order).to_json) }
|
8
8
|
|
9
9
|
it "calls Signifyd::Case#create with the correct params" do
|
10
|
-
Signifyd::Case.
|
11
|
-
CreateSignifydCase.
|
10
|
+
expect(Signifyd::Case).to receive(:create).with(json, SpreeSignifyd::Config[:api_key])
|
11
|
+
CreateSignifydCase.perform_now(order.id)
|
12
12
|
end
|
13
13
|
|
14
14
|
it "calls Signifyd::Case#create with the correct params" do
|
15
|
-
Signifyd::Case.
|
16
|
-
CreateSignifydCase.
|
15
|
+
expect(Signifyd::Case).to receive(:create).with(json, SpreeSignifyd::Config[:api_key])
|
16
|
+
CreateSignifydCase.perform_now(order.number)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -50,7 +50,7 @@ module SpreeSignifyd
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'readies all of the shipments' do
|
53
|
-
order.shipments.each { |shipment| shipment.
|
53
|
+
order.shipments.each { |shipment| expect(shipment).to receive(:ready!) }
|
54
54
|
approve
|
55
55
|
end
|
56
56
|
|
@@ -61,12 +61,23 @@ module SpreeSignifyd
|
|
61
61
|
expect { approve }.to change { order.approved_at }
|
62
62
|
end
|
63
63
|
end
|
64
|
+
|
65
|
+
context "with backordered stock" do
|
66
|
+
before do
|
67
|
+
order.inventory_units.first.update(state: 'backordered')
|
68
|
+
order.reload
|
69
|
+
end
|
70
|
+
|
71
|
+
it "does not attempt invalid state changes" do
|
72
|
+
approve
|
73
|
+
expect(order.reload.shipments.first).to be_pending
|
74
|
+
end
|
75
|
+
end
|
64
76
|
end
|
65
77
|
|
66
78
|
describe ".create_case" do
|
67
79
|
it 'enqueues in resque' do
|
68
|
-
expect(
|
69
|
-
SpreeSignifyd.create_case(order_number: 111)
|
80
|
+
expect { SpreeSignifyd.create_case(order_number: 111) }.to have_enqueued_job(SpreeSignifyd::CreateSignifydCase)
|
70
81
|
end
|
71
82
|
end
|
72
83
|
|