solidus_signifyd 0.1.1

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE +26 -0
  7. data/README.md +32 -0
  8. data/Rakefile +21 -0
  9. data/app/controllers/spree/api/spree_signifyd/orders_controller.rb +54 -0
  10. data/app/models/spree/order_decorator.rb +1 -0
  11. data/app/models/spree/signifyd_configuration.rb +6 -0
  12. data/app/models/spree_signifyd/order_concerns.rb +23 -0
  13. data/app/models/spree_signifyd/order_score.rb +9 -0
  14. data/app/models/spree_signifyd/shipment_decorator.rb +11 -0
  15. data/app/overrides/admin_order_signifyd_risk_analysis.rb +6 -0
  16. data/app/serializers/spree_signifyd/address_serializer.rb +20 -0
  17. data/app/serializers/spree_signifyd/billing_address_serializer.rb +13 -0
  18. data/app/serializers/spree_signifyd/credit_card_serializer.rb +25 -0
  19. data/app/serializers/spree_signifyd/delivery_address_serializer.rb +14 -0
  20. data/app/serializers/spree_signifyd/line_item_serializer.rb +25 -0
  21. data/app/serializers/spree_signifyd/order_serializer.rb +59 -0
  22. data/app/serializers/spree_signifyd/user_serializer.rb +44 -0
  23. data/app/views/spree/admin/orders/_signifyd_score.html.erb +10 -0
  24. data/bin/rails +7 -0
  25. data/circle.yml +6 -0
  26. data/config/locales/en.yml +7 -0
  27. data/config/routes.rb +7 -0
  28. data/db/migrate/20140819203000_add_signifyd_score_to_orders.rb +5 -0
  29. data/db/migrate/20140826202644_set_considered_risky_default_value.rb +5 -0
  30. data/db/migrate/20150206151312_create_spree_signifyd_order_scores.rb +11 -0
  31. data/db/migrate/20150206193231_transfer_spree_orders_signifyd_score_data.rb +15 -0
  32. data/db/migrate/20150211202803_remove_spree_orders_signifyd_score_column.rb +5 -0
  33. data/lib/generators/solidus_signifyd/install/install_generator.rb +26 -0
  34. data/lib/generators/solidus_signifyd/install/templates/solidus_signifyd.rb +3 -0
  35. data/lib/solidus_signifyd.rb +1 -0
  36. data/lib/spree_signifyd.rb +37 -0
  37. data/lib/spree_signifyd/create_signifyd_case.rb +13 -0
  38. data/lib/spree_signifyd/engine.rb +24 -0
  39. data/lib/spree_signifyd/request_verifier.rb +14 -0
  40. data/solidus_signifyd.gemspec +36 -0
  41. data/spec/controllers/spree/api/spree_signifyd/orders_controller_spec.rb +202 -0
  42. data/spec/lib/spree_signifyd/create_signifyd_case_spec.rb +20 -0
  43. data/spec/lib/spree_signifyd/request_verifier_spec.rb +27 -0
  44. data/spec/lib/spree_signifyd_spec.rb +66 -0
  45. data/spec/models/spree/order_spec.rb +51 -0
  46. data/spec/models/spree/shipment_spec.rb +59 -0
  47. data/spec/serializers/spree_signifyd/address_serializer_spec.rb +42 -0
  48. data/spec/serializers/spree_signifyd/billing_address_serializer.rb +12 -0
  49. data/spec/serializers/spree_signifyd/credit_card_serializer_spec.rb +26 -0
  50. data/spec/serializers/spree_signifyd/delivery_address_serializer_spec.rb +13 -0
  51. data/spec/serializers/spree_signifyd/line_item_serializer_spec.rb +26 -0
  52. data/spec/serializers/spree_signifyd/order_serializer_spec.rb +72 -0
  53. data/spec/serializers/spree_signifyd/user_serializer_spec.rb +53 -0
  54. data/spec/spec_helper.rb +63 -0
  55. metadata +294 -0
@@ -0,0 +1,10 @@
1
+ <tr>
2
+ <td><strong><%= Spree.t(:"risk.signifyd_score") %>:</strong></td>
3
+ <td class="align-center">
4
+ <% if @order.signifyd_order_score.nil? %>
5
+ <span class="state pending"><%= Spree.t(:"risk.awaiting_score") %></span>
6
+ <% else %>
7
+ <span class="state void"><%= @order.signifyd_order_score.score %></span>
8
+ <% end %>
9
+ </td>
10
+ </tr>
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/spree_signifyd/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
data/circle.yml ADDED
@@ -0,0 +1,6 @@
1
+ machine:
2
+ ruby:
3
+ version: 2.1.5
4
+ test:
5
+ pre:
6
+ - bundle exec rake test_app
@@ -0,0 +1,7 @@
1
+ en:
2
+ spree:
3
+ admin:
4
+ risk:
5
+ approved_by_admin: "Approved By Admin"
6
+ awaiting_score: "Awaiting Score"
7
+ signifyd_score: "Signifyd Score"
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ namespace :api, defaults: { format: 'json' } do
3
+ namespace :spree_signifyd do
4
+ post '/orders', to: 'orders#update'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddSignifydScoreToOrders < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_orders, :signifyd_score, :integer
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class SetConsideredRiskyDefaultValue < ActiveRecord::Migration
2
+ def change
3
+ change_column_default(:spree_orders, :considered_risky, true) if Spree::Order.column_names.include? "considered_risky"
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSpreeSignifydOrderScores < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_signifyd_order_scores do |t|
4
+ t.integer :order_id
5
+ t.integer :score
6
+ t.timestamps null: true
7
+ end
8
+
9
+ add_index :spree_signifyd_order_scores, :order_id, unique: true
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ class TransferSpreeOrdersSignifydScoreData < ActiveRecord::Migration
2
+ disable_ddl_transaction!
3
+
4
+ def up
5
+ Spree::Order.connection.execute(<<-SQL)
6
+ insert into spree_signifyd_order_scores (order_id, score, created_at, updated_at)
7
+ select o.id, o.signifyd_score, '#{Time.now.to_s(:db)}', '#{Time.now.to_s(:db)}'
8
+ from spree_orders o
9
+ left join spree_signifyd_order_scores
10
+ on o.id = spree_signifyd_order_scores.order_id
11
+ where o.signifyd_score is not null -- where the order has a score...
12
+ and spree_signifyd_order_scores.id is null -- ...but the new table does not
13
+ SQL
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class RemoveSpreeOrdersSignifydScoreColumn < ActiveRecord::Migration
2
+ def change
3
+ remove_column :spree_orders, :signifyd_score, :integer
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ module SolidusSignifyd
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :auto_run_migrations, type: :boolean, default: false
5
+
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def add_initializer
9
+ copy_file "solidus_signifyd.rb", "config/initializers/solidus_signifyd.rb"
10
+ end
11
+
12
+ def add_migrations
13
+ run 'bundle exec rake railties:install:migrations FROM=solidus_signifyd'
14
+ end
15
+
16
+ def run_migrations
17
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
18
+ if run_migrations
19
+ run 'bundle exec rake db:migrate'
20
+ else
21
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ SpreeSignifyd::Config.configure do |config|
2
+ config.api_key = "YOUR SIGNIFYD API KEY"
3
+ end
@@ -0,0 +1 @@
1
+ require "spree_signifyd"
@@ -0,0 +1,37 @@
1
+ require 'spree_core'
2
+ require 'signifyd'
3
+ require 'spree_signifyd/create_signifyd_case'
4
+ require 'spree_signifyd/engine'
5
+ require 'spree_signifyd/request_verifier'
6
+ require 'resque'
7
+ require 'devise'
8
+
9
+ module SpreeSignifyd
10
+
11
+ module_function
12
+
13
+ def set_score(order:, score:)
14
+ if order.signifyd_order_score
15
+ order.signifyd_order_score.update!(score: score)
16
+ else
17
+ order.create_signifyd_order_score!(score: score)
18
+ end
19
+ end
20
+
21
+ def approve(order:)
22
+ order.contents.approve(name: self.name)
23
+ order.shipments.each { |shipment| shipment.ready! unless shipment.ready? }
24
+ order.updater.update_shipment_state
25
+ order.save!
26
+ end
27
+
28
+ def create_case(order_number:)
29
+ Rails.logger.info "Queuing Signifyd case creation event: #{order_number}"
30
+ Resque.enqueue(SpreeSignifyd::CreateSignifydCase, order_number)
31
+ end
32
+
33
+ def score_above_threshold?(score)
34
+ score > SpreeSignifyd::Config[:signifyd_score_threshold]
35
+ end
36
+
37
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeSignifyd
2
+ class CreateSignifydCase
3
+ @queue = :spree_backend_high
4
+
5
+ def self.perform(order_number_or_id)
6
+ Rails.logger.info "Processing Signifyd case creation event: #{order_number_or_id}"
7
+ order = Spree::Order.find_by(number: order_number_or_id) || Spree::Order.find(order_number_or_id)
8
+ order_data = JSON.parse(OrderSerializer.new(order).to_json)
9
+ Signifyd::Case.create(order_data, SpreeSignifyd::Config[:api_key])
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeSignifyd
2
+ class Engine < Rails::Engine
3
+ require "spree/core"
4
+ isolate_namespace Spree
5
+ engine_name "solidus_signifyd"
6
+
7
+ # use rspec for tests
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer "spree.signifyd.environment", before: :load_config_initializers do |app|
13
+ SpreeSignifyd::Config = Spree::SignifydConfiguration.new
14
+ end
15
+
16
+ def self.activate
17
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator*.rb")) do |c|
18
+ Rails.configuration.cache_classes ? require(c) : load(c)
19
+ end
20
+ end
21
+
22
+ config.to_prepare &method(:activate).to_proc
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module SpreeSignifyd
2
+ module RequestVerifier
3
+
4
+ def encode_request(request_body)
5
+ request_body.force_encoding('ISO-8859-1').encode('UTF-8')
6
+ end
7
+
8
+ def build_sha(key, message)
9
+ sha256 = OpenSSL::Digest::SHA256.new
10
+ digest = OpenSSL::HMAC.digest(sha256, key, message)
11
+ Base64.encode64(digest).strip
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: UTF-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.platform = Gem::Platform::RUBY
5
+ s.name = "solidus_signifyd"
6
+ s.version = "0.1.1"
7
+ s.summary = "Solidus extension for communicating with Signifyd to check orders for fraud."
8
+ s.description = s.summary
9
+
10
+ s.author = "Bonobos"
11
+ s.email = "engineering@bonobos.com"
12
+ s.homepage = "http://www.bonobos.com"
13
+
14
+ s.required_ruby_version = ">= 2.1"
15
+ s.license = %q{BSD-3}
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.require_path = "lib"
20
+ s.requirements << "none"
21
+
22
+ s.add_dependency "active_model_serializers", "0.9.3"
23
+ s.add_dependency "resque", "~> 1.25.1"
24
+ s.add_dependency "signifyd", "~> 0.1.5"
25
+ s.add_dependency "solidus", "~> 1.0.0"
26
+ s.add_dependency "devise"
27
+
28
+ s.add_development_dependency "rspec-rails", "~> 2.13"
29
+ s.add_development_dependency "simplecov"
30
+ s.add_development_dependency "sqlite3"
31
+ s.add_development_dependency "sass-rails"
32
+ s.add_development_dependency "coffee-rails"
33
+ s.add_development_dependency "database_cleaner"
34
+ s.add_development_dependency "factory_girl"
35
+ s.add_development_dependency "ffaker"
36
+ end
@@ -0,0 +1,202 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree::Api::SpreeSignifyd
4
+
5
+ describe OrdersController do
6
+ describe 'POST #update' do
7
+
8
+ let(:order_number) { "19418" }
9
+ let!(:order) { create(:completed_order_with_totals, number: order_number) }
10
+ let!(:user) { create(:user) }
11
+ let(:signifyd_sha) { 'sdGXFLSPZi5hTt8ZCVR9FeNMrsfmOblEIkpV2cCVLxM=' }
12
+
13
+ let(:body) {
14
+ {
15
+ "analysisUrl" => "https://signifyd.com/v2/cases/1/analysis",
16
+ "entriesUrl" => "https://signifyd.com/v2/cases/1/entries",
17
+ "notesUrl" => "https://signifyd.com/v2/cases/1/notes",
18
+ "orderUrl" => "https://signifyd.com/v2/cases/1/order",
19
+ "status" => "DISMISSED",
20
+ "uuid" => "709b9107-eda0-4cdd-bdac-a82f51a8a3f3",
21
+ "headline" => "John Smith",
22
+ "reviewDisposition" => nil,
23
+ "associatedTeam" => {
24
+ "teamName" => "anyTeam",
25
+ "teamId" => 26,
26
+ "getAutoDismiss" => true,
27
+ "getTeamDismissalDays" => 2
28
+ },
29
+ "orderId" => order_number,
30
+ "orderDate" => "2013-06-17T06:20:47-0700",
31
+ "orderAmount" => 365.99,
32
+ "createdAt" => "2013-11-05T14:23:26-0800",
33
+ "updatedAt" => "2013-11-05T14:23:26-0800",
34
+ "adjustedScore" => 262.6666666666667,
35
+ "investigationId" => 1,
36
+ "score" => 262.6666666666667,
37
+ "caseId" => 1
38
+ }
39
+ }
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]
45
+ SpreeSignifyd::Config[:api_key] = 'ABCDE'
46
+ example.run
47
+ SpreeSignifyd::Config[:api_key] = previous_api_key
48
+ end
49
+
50
+ routes { Spree::Core::Engine.routes }
51
+
52
+ subject { post :update, body.to_json }
53
+
54
+ context "invalid sha" do
55
+ let(:signifyd_sha) { "INVALID" }
56
+
57
+ it "does not set signifyd_score" do
58
+ subject
59
+ order.reload
60
+ expect(order.signifyd_order_score).to eq nil
61
+ end
62
+
63
+ it "responds with 401" do
64
+ subject
65
+ expect(response.code.to_i).to eq 401
66
+ end
67
+ end
68
+
69
+ context "valid sha" do
70
+ context "invalid order number" do
71
+ before(:each) { order.destroy! }
72
+
73
+ it "responds with a 404" do
74
+ subject
75
+ expect(response.code.to_i).to eq 404
76
+ end
77
+ end
78
+
79
+ context "the order has been shipped" do
80
+
81
+ it "returns without trying to act on the order" do
82
+ Spree::Order.any_instance.stub(:shipped?).and_return(true)
83
+ expect(SpreeSignifyd).not_to receive(:approve)
84
+ expect(Spree::Order.any_instance).not_to receive(:cancel!)
85
+ expect { subject }.not_to raise_error
86
+ expect(response.status).to eq(200)
87
+ end
88
+ end
89
+
90
+ context "the order has been canceled" do
91
+ before(:each) { order.cancel! }
92
+
93
+ it "returns without trying to act on the order" do
94
+ expect(SpreeSignifyd).not_to receive(:approve)
95
+ expect(Spree::Order.any_instance).not_to receive(:cancel!)
96
+ expect { subject }.not_to raise_error
97
+ expect(response.status).to eq(200)
98
+ end
99
+ end
100
+
101
+ context "valid order number" do
102
+ it "sets the order's signifyd_score" do
103
+ subject
104
+ order.reload
105
+ expect(order.signifyd_order_score.score).to eq 262
106
+ end
107
+
108
+ it "responds with 200" do
109
+ subject
110
+ expect(response.code.to_i).to eq 200
111
+ end
112
+
113
+ context "reviewDisposition is FRAUDULENT" do
114
+ let(:signifyd_sha) { "ulHF48lbFO3M6UBMSi1tAroJWADeSggrr6V7ND8hBx0=" }
115
+
116
+ before(:each) do
117
+ @original_review_disposition = body['reviewDiposition']
118
+ body['reviewDisposition'] = 'FRAUDULENT'
119
+ end
120
+
121
+ after(:each) { body['reviewDiposition'] = @original_review_disposition }
122
+
123
+ it 'cancels the order' do
124
+ Spree::Order.any_instance.should_receive(:cancel!)
125
+ subject
126
+ end
127
+ end
128
+
129
+ context "reviewDisposition is not FRAUDULENT" do
130
+ context "the order has already been approved" do
131
+
132
+ before(:each) { order.update_attribute(:approved_at, Time.now) }
133
+
134
+ it "does not call approve" do
135
+ expect(SpreeSignifyd).not_to receive(:approve)
136
+ subject
137
+ end
138
+ end
139
+
140
+ context "the order has not yet been approved" do
141
+ context "the reviewDisposition is GOOD" do
142
+ let(:signifyd_sha) { "wZIjgRQoDMWe0W4VoE5TJEoHf8ZcY9UeXY1lnGP+pfg=" }
143
+
144
+ before(:each) do
145
+ @original_review_disposition = body['reviewDisposition']
146
+ body['reviewDisposition'] = 'GOOD'
147
+ end
148
+
149
+ after(:each) { body['reviewDisposition'] = @original_review_disposition }
150
+
151
+ it "calls approve" do
152
+ expect(SpreeSignifyd).to receive(:approve).with(order: order)
153
+ subject
154
+ end
155
+ end
156
+
157
+ context "the reviewDisposition is not GOOD" do
158
+ it "does not call approve" do
159
+ expect(SpreeSignifyd).not_to receive(:approve)
160
+ subject
161
+ end
162
+ end
163
+
164
+ context "the order is not risky" do
165
+ let(:signifyd_sha) { "ZI7bSCavfy6pWogJZ7nq2LbLLojcfcy9kjF02WHO4nM=" }
166
+
167
+ before(:each) do
168
+ @original_score = body['adjustedScore']
169
+ body['adjustedScore'] = SpreeSignifyd::Config[:signifyd_score_threshold] + 1
170
+ end
171
+
172
+ after(:each) { body['adjustedScore'] = @original_score }
173
+
174
+ it "approves the order" do
175
+ expect(SpreeSignifyd).to receive(:approve).with(order: order)
176
+ subject
177
+ end
178
+ end
179
+
180
+ context "the order is risky" do
181
+
182
+ let(:signifyd_sha) { "YcEDVtPBAXcgQ9fJgBMSoBWy9CVpc6pnN6YzCbtD85E=" }
183
+
184
+ before(:each) do
185
+ @original_score = body['adjustedScore']
186
+ body['adjustedScore'] = SpreeSignifyd::Config[:signifyd_score_threshold] - 1
187
+ end
188
+
189
+ after(:each) { body['adjustedScore'] = @original_score }
190
+
191
+ it "does not approve the order" do
192
+ expect(SpreeSignifyd).not_to receive(:approve)
193
+ subject
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end