solidus_signifyd 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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