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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e7c186c4c9b430760dabce1a20c7b0d6619241de
4
+ data.tar.gz: fa6036cd2c018e312763b158926f201f26973adf
5
+ SHA512:
6
+ metadata.gz: 49806f09e4991a97f8ac9605c8b9ecbac09384477f9391649a9f79e97c553baeee02e676cb223ed62dc16264d9992b1e45ec11f236b23ae6e68dfa52d83df917
7
+ data.tar.gz: 7cdc4844d2b1649be005514ad4cc213316faabda99675bfa24e614b9a32ff1d5554248cb71ea7e208cc63adbdf94216aa0d40696933a469bbb6999ca1f3a862c
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ *.gem
2
+ \#*
3
+ *~
4
+ .#*
5
+ .DS_Store
6
+ .idea
7
+ .project
8
+ .sass-cache
9
+ coverage
10
+ Gemfile.lock
11
+ tmp
12
+ nbproject
13
+ pkg
14
+ *.swp
15
+ spec/dummy
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.7
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "solidus", git: "git@github.com:solidusio/solidus.git", branch: "v1.0"
4
+ gem "solidus_auth_devise", "~> 1.0"
5
+
6
+ group :development, :test do
7
+ gem "pry-rails"
8
+ end
9
+
10
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2014 Bonobos, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Bonobos nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ Solidus Signifyd
2
+ ================
3
+
4
+ Integration with Signifyd that implements a fraud check prior to marking a
5
+ shipment as ready to be shipped.
6
+
7
+ Installation
8
+ ------------
9
+
10
+ In your Gemfile:
11
+
12
+ ```ruby
13
+ gem "solidus_signifyd"
14
+ ```
15
+
16
+ Bundle your dependencies and run the installation generator:
17
+
18
+ ```shell
19
+ bundle
20
+ bundle exec rails g solidus_signifyd:install
21
+ ```
22
+
23
+ Testing
24
+ -------
25
+
26
+ First bundle your dependencies, then run `rake`. `rake` will default to
27
+ building the dummy app if it does not exist, then it will run specs. The dummy
28
+ app can be regenerated by using `rake test_app`.
29
+
30
+ ```shell
31
+ bundle exec rake
32
+ ```
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir["spec/dummy"].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir("../../")
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'solidus_signifyd'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,54 @@
1
+ module Spree::Api::SpreeSignifyd
2
+ class OrdersController < ActionController::Base
3
+ include SpreeSignifyd::RequestVerifier
4
+
5
+ respond_to :json
6
+
7
+ before_filter :authorize, :load_order, :order_canceled_or_shipped
8
+
9
+ def update
10
+ SpreeSignifyd.set_score(order: @order, score: score)
11
+
12
+ if is_fraudulent?
13
+ @order.cancel!
14
+ elsif should_approve?
15
+ SpreeSignifyd.approve(order: @order)
16
+ end
17
+
18
+ render nothing: true, status: 200
19
+ end
20
+
21
+ private
22
+
23
+ def authorize
24
+ request_sha = request.headers['HTTP_HTTP_X_SIGNIFYD_HMAC_SHA256']
25
+ computed_sha = build_sha(SpreeSignifyd::Config[:api_key], encode_request(request.raw_post))
26
+
27
+ head 401 unless Devise.secure_compare(request_sha, computed_sha)
28
+ end
29
+
30
+ def load_order
31
+ head 404 unless @order = Spree::Order.find_by(number: body['orderId'])
32
+ end
33
+
34
+ def order_canceled_or_shipped
35
+ head 200 if @order.shipped? || @order.canceled?
36
+ end
37
+
38
+ def body
39
+ @body ||= JSON.parse(request.raw_post)
40
+ end
41
+
42
+ def is_fraudulent?
43
+ body['reviewDisposition'] == 'FRAUDULENT'
44
+ end
45
+
46
+ def should_approve?
47
+ body['reviewDisposition'] == 'GOOD' || SpreeSignifyd.score_above_threshold?(score)
48
+ end
49
+
50
+ def score
51
+ body['adjustedScore']
52
+ end
53
+ end
54
+ end
@@ -0,0 +1 @@
1
+ Spree::Order.include SpreeSignifyd::OrderConcerns
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ class SignifydConfiguration < Preferences::Configuration
3
+ preference :api_key, :string
4
+ preference :signifyd_score_threshold, :integer, default: 500 # Signifyd's recommended threshold
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeSignifyd::OrderConcerns
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ Spree::Order.state_machine.after_transition to: :complete, unless: :approved? do |order, transition|
6
+ SpreeSignifyd.create_case(order_number: order.number)
7
+ end
8
+
9
+ has_one :signifyd_order_score, class_name: "SpreeSignifyd::OrderScore"
10
+
11
+ prepend(InstanceMethods)
12
+ end
13
+
14
+ module InstanceMethods
15
+ def is_risky?
16
+ !(awaiting_approval? || approved?)
17
+ end
18
+
19
+ def awaiting_approval?
20
+ !signifyd_order_score
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeSignifyd
2
+ class OrderScore < ActiveRecord::Base
3
+ self.table_name = :spree_signifyd_order_scores
4
+
5
+ belongs_to :order, class_name: "Spree::Order"
6
+ validates :score, numericality: true
7
+ validates :order, presence: true
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeSignifyd
2
+ module ShipmentDecorator
3
+
4
+ def determine_state(order)
5
+ return 'pending' if (pending? || canceled?) && !order.approved?
6
+ super(order)
7
+ end
8
+ end
9
+ end
10
+
11
+ Spree::Shipment.prepend SpreeSignifyd::ShipmentDecorator
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(
2
+ virtual_path: "spree/admin/orders/_risk_analysis",
3
+ name: "admin_order_signifyd_risk_analysis",
4
+ insert_bottom: "[data-hook='order_details_adjustments']",
5
+ partial: "spree/admin/orders/signifyd_score",
6
+ )
@@ -0,0 +1,20 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class AddressSerializer < ActiveModel::Serializer
5
+ self.root = false
6
+
7
+ attributes :address
8
+
9
+ def address
10
+ {
11
+ 'streetAddress' => object.address1,
12
+ 'unit' => object.address2,
13
+ 'city' => object.city,
14
+ 'provinceCode' => object.state_text,
15
+ 'postalCode' => object.zipcode,
16
+ 'countryCode' => object.country.iso
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class BillingAddressSerializer < AddressSerializer
5
+ self.root = false
6
+
7
+ def attributes
8
+ hash = {}
9
+ hash['billingAddress'] = address
10
+ hash
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class CreditCardSerializer < ActiveModel::Serializer
5
+ self.root = false
6
+
7
+ attributes :cardHolderName, :last4, :expiryMonth, :expiryYear
8
+
9
+ def cardHolderName
10
+ "#{object.first_name} #{object.last_name}"
11
+ end
12
+
13
+ def last4
14
+ object.last_digits
15
+ end
16
+
17
+ def expiryMonth
18
+ object.month
19
+ end
20
+
21
+ def expiryYear
22
+ object.year
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class DeliveryAddressSerializer < AddressSerializer
5
+ self.root = false
6
+
7
+ def attributes
8
+ hash = {}
9
+ hash['deliveryAddress'] = address
10
+ hash['fullName'] = object.full_name
11
+ hash
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class LineItemSerializer < ActiveModel::Serializer
5
+ self.root = false
6
+
7
+ attributes :itemId, :itemName, :itemQuantity, :itemPrice
8
+
9
+ def itemId
10
+ object.variant_id
11
+ end
12
+
13
+ def itemName
14
+ object.name
15
+ end
16
+
17
+ def itemQuantity
18
+ object.quantity
19
+ end
20
+
21
+ def itemPrice
22
+ object.price
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class OrderSerializer < ActiveModel::Serializer
5
+ self.root = false
6
+
7
+ attributes :purchase, :recipient, :card
8
+ has_one :user, serializer: SpreeSignifyd::UserSerializer, root: "userAccount"
9
+
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
+ }
21
+ end
22
+
23
+ def recipient
24
+ recipient = SpreeSignifyd::DeliveryAddressSerializer.new(object.ship_address).serializable_object
25
+ recipient[:confirmationEmail] = object.email
26
+ recipient[:fullName] = object.ship_address.full_name
27
+ recipient
28
+ end
29
+
30
+ def card
31
+ payment_source = latest_payment.try(:source)
32
+ card = {}
33
+
34
+ if payment_source.present? && payment_source.instance_of?(Spree::CreditCard)
35
+ card = CreditCardSerializer.new(payment_source).serializable_object
36
+ card.merge!(SpreeSignifyd::BillingAddressSerializer.new(object.bill_address).serializable_object)
37
+ end
38
+
39
+ card
40
+ end
41
+
42
+ private
43
+
44
+ def products
45
+ order_products = []
46
+
47
+ object.line_items.each do |li|
48
+ serialized_line_item = SpreeSignifyd::LineItemSerializer.new(li).serializable_object
49
+ order_products << serialized_line_item
50
+ end
51
+
52
+ order_products
53
+ end
54
+
55
+ def latest_payment
56
+ object.payments.order(:created_at, :id).last
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ require 'active_model/serializer'
2
+
3
+ module SpreeSignifyd
4
+ class UserSerializer < ActiveModel::Serializer
5
+ self.root = false
6
+
7
+ attributes :emailAddress, :username, :createdDate, :lastUpdateDate, :lastOrderId, :aggregateOrderCount, :aggregateOrderDollars
8
+
9
+ def emailAddress
10
+ object.email
11
+ end
12
+
13
+ def username
14
+ object.email
15
+ end
16
+
17
+ def createdDate
18
+ object.created_at.utc.iso8601
19
+ end
20
+
21
+ def lastUpdateDate
22
+ object.updated_at.utc.iso8601
23
+ end
24
+
25
+ def lastOrderId
26
+ completed_orders.order("completed_at DESC").second.try(:number)
27
+ end
28
+
29
+ def aggregateOrderCount
30
+ completed_orders.count
31
+ end
32
+
33
+ def aggregateOrderDollars
34
+ completed_orders.sum(:total)
35
+ end
36
+
37
+ private
38
+
39
+ def completed_orders
40
+ @order ||= object.orders.complete
41
+ end
42
+
43
+ end
44
+ end