solidus_signifyd 1.0.1 → 1.1.0

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +21 -0
  3. data/Gemfile +10 -3
  4. data/README.md +58 -1
  5. data/app/controllers/spree/api/spree_signifyd/orders_controller.rb +1 -1
  6. data/app/models/spree/signifyd_configuration.rb +1 -0
  7. data/app/models/spree_signifyd/order_concerns.rb +18 -1
  8. data/app/models/spree_signifyd/shipment_decorator.rb +1 -1
  9. data/app/serializers/spree_signifyd/credit_card_serializer.rb +9 -9
  10. data/app/serializers/spree_signifyd/order_serializer.rb +22 -10
  11. data/app/serializers/spree_signifyd/user_serializer.rb +13 -2
  12. data/lib/spree_signifyd.rb +2 -3
  13. data/lib/spree_signifyd/create_signifyd_case.rb +3 -4
  14. data/lib/spree_signifyd/engine.rb +1 -0
  15. data/solidus_signifyd.gemspec +4 -4
  16. data/spec/controllers/spree/api/spree_signifyd/orders_controller_spec.rb +6 -10
  17. data/spec/lib/spree_signifyd/create_signifyd_case_spec.rb +4 -4
  18. data/spec/lib/spree_signifyd_spec.rb +14 -3
  19. data/spec/models/spree/order_spec.rb +38 -11
  20. data/spec/models/spree/shipment_spec.rb +33 -35
  21. data/spec/serializers/spree_signifyd/billing_address_serializer.rb +1 -1
  22. data/spec/serializers/spree_signifyd/credit_card_serializer_spec.rb +2 -2
  23. data/spec/serializers/spree_signifyd/delivery_address_serializer_spec.rb +2 -2
  24. data/spec/serializers/spree_signifyd/order_serializer_spec.rb +58 -17
  25. data/spec/serializers/spree_signifyd/user_serializer_spec.rb +9 -1
  26. data/spec/spec_helper.rb +7 -0
  27. data/spec/support/api_schema_matcher.rb +9 -0
  28. data/spec/support/schemas/v2/case.json +305 -0
  29. metadata +46 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d4a72f66712b1ec31d58c43b6376ec26d73fa0b3
4
- data.tar.gz: 82f7c527003108aa93f77df909191963ab0643b8
3
+ metadata.gz: 5c7e1bb9ec394555d95c0bd04879ed53f60f36a4
4
+ data.tar.gz: 1ea89e5ec06118d867ba7fd1582785b64caaf129
5
5
  SHA512:
6
- metadata.gz: f4ac359792627cf09505a0b68aa7ec26c7b627ef4d3de66c2d009882aac691e99a3aed2703d2828c23d8e35f436c2118997b211d615dfacf6f72d6e60c7f8b15
7
- data.tar.gz: 6c96d211b2d6f697cbdd100d635eaa8b98845c5dd8116f71e43eb0125a2089dd0d3c57583be3237e7a247187ba2d4feb30dc8f419ecce50792b963a2640417f4
6
+ metadata.gz: 601b96f317fb0f2b439f4550c910c1bcbfafe6cd0e1419f3a651c2aa2b7cb9ae956fa717b1abcc828628f11a63c45f976d7c84cf682b5f764fe1fe0772974885
7
+ data.tar.gz: fb0afbf0e113611b9f45e9e0011909acfa441590681910aead26d0a01784e646bb7752b71022bdfe57c96147095bf1c2775368f319a0ce5aa75388b989495804
@@ -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 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gem "solidus", github: "solidusio/solidus", branch: "master"
4
- gem "solidus_auth_devise", "~> 1.0"
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
- [![Circle CI](https://circleci.com/gh/solidusio/solidus_signifyd.svg?style=shield)](https://circleci.com/gh/solidusio/solidus_signifyd/tree/master)
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['HTTP_HTTP_X_SIGNIFYD_HMAC_SHA256']
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
- SpreeSignifyd.create_case(order_number: order.number)
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
@@ -2,7 +2,7 @@ module SpreeSignifyd
2
2
  module ShipmentDecorator
3
3
 
4
4
  def determine_state(order)
5
- return 'pending' if (pending? || canceled?) && !order.approved?
5
+ return 'pending' if pending? && !order.approved?
6
6
  super(order)
7
7
  end
8
8
  end
@@ -4,7 +4,15 @@ module SpreeSignifyd
4
4
  class CreditCardSerializer < ActiveModel::Serializer
5
5
  self.root = false
6
6
 
7
- attributes :cardHolderName, :last4, :expiryMonth, :expiryYear
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
- 'browserIpAddress' => object.last_ip_address,
13
- 'orderId' => object.number,
14
- 'createdAt' => object.completed_at.utc.iso8601,
15
- 'currency' => object.currency,
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, :lastOrderId, :aggregateOrderCount, :aggregateOrderDollars
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
@@ -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.pending? }
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
- Resque.enqueue(SpreeSignifyd::CreateSignifydCase, order_number)
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
- @queue = :spree_backend_high
2
+ class CreateSignifydCase < ActiveJob::Base
3
+ queue_as :default
4
4
 
5
- def self.perform(order_number_or_id)
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
@@ -11,6 +11,7 @@ module SpreeSignifyd
11
11
 
12
12
  initializer "spree.signifyd.environment", before: :load_config_initializers do |app|
13
13
  SpreeSignifyd::Config = Spree::SignifydConfiguration.new
14
+ SpreeSignifyd::Config.use_static_preferences!
14
15
  end
15
16
 
16
17
  def self.activate
@@ -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.1"
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", "~> 1.0"
24
+ s.add_dependency "solidus", [">= 1.0", "< 3"]
26
25
  s.add_dependency "devise"
27
26
 
28
- s.add_development_dependency "rspec-rails", "~> 2.13"
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 { request.headers['HTTP_HTTP_X_SIGNIFYD_HMAC_SHA256'] = signifyd_sha }
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.any_instance.stub(:shipped?).and_return(true)
78
+ allow_any_instance_of(Spree::Order).to receive(:shipped?).and_return(true)
83
79
  expect(SpreeSignifyd).not_to receive(:approve)
84
- expect(Spree::Order.any_instance).not_to receive(:cancel!)
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
- expect(Spree::Order.any_instance).not_to receive(:cancel!)
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.any_instance.should_receive(:cancel!)
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.should_receive(:create).with(json, SpreeSignifyd::Config[:api_key])
11
- CreateSignifydCase.perform(order.id)
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.should_receive(:create).with(json, SpreeSignifyd::Config[:api_key])
16
- CreateSignifydCase.perform(order.number)
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.should_receive(:ready!) }
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(Resque).to receive(:enqueue).with(SpreeSignifyd::CreateSignifydCase, 111)
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