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.
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