spree_affirm 0.2.19 → 0.2.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -0
  3. data/Gemfile +19 -0
  4. data/LICENSE +26 -0
  5. data/README.md +47 -0
  6. data/Rakefile +15 -0
  7. data/Versionfile +5 -0
  8. data/app/assets/javascripts/spree/backend/spree_affirm.js +3 -0
  9. data/app/assets/javascripts/spree/frontend/spree_affirm.js +2 -0
  10. data/app/assets/stylesheets/spree/backend/spree_affirm.css +4 -0
  11. data/app/assets/stylesheets/spree/frontend/spree_affirm.css +4 -0
  12. data/app/controllers/spree/affirm_controller.rb +127 -0
  13. data/app/models/affirm/address_validator.rb +25 -0
  14. data/app/models/spree/affirm_checkout.rb +147 -0
  15. data/app/models/spree/gateway/affirm.rb +58 -0
  16. data/app/views/spree/admin/log_entries/_affirm.html.erb +4 -0
  17. data/app/views/spree/admin/log_entries/index.html.erb +28 -0
  18. data/app/views/spree/admin/payments/source_forms/_affirm.html.erb +19 -0
  19. data/app/views/spree/admin/payments/source_views/_affirm.html.erb +12 -0
  20. data/app/views/spree/checkout/affirm/_learn_more.html.erb +0 -0
  21. data/app/views/spree/checkout/payment/_affirm.html.erb +190 -0
  22. data/bin/rails +7 -0
  23. data/config/locales/en.yml +31 -0
  24. data/config/routes.rb +5 -0
  25. data/db/migrate/20140514194315_create_affirm_checkout.rb +10 -0
  26. data/lib/active_merchant/billing/affirm.rb +161 -0
  27. data/lib/generators/spree_affirm/install/install_generator.rb +21 -0
  28. data/lib/spree_affirm.rb +2 -0
  29. data/lib/spree_affirm/engine.rb +27 -0
  30. data/lib/spree_affirm/factories.rb +5 -0
  31. data/lib/spree_affirm/factories/affirm_checkout_factory.rb +219 -0
  32. data/lib/spree_affirm/factories/affirm_payment_method_factory.rb +8 -0
  33. data/lib/spree_affirm/factories/payment_factory.rb +10 -0
  34. data/lib/spree_affirm/version.rb +3 -0
  35. data/spec/controllers/affirm_controller_spec.rb +138 -0
  36. data/spec/lib/active_merchant/billing/affirm_spec.rb +294 -0
  37. data/spec/models/affirm_address_validator_spec.rb +13 -0
  38. data/spec/models/spree_affirm_checkout_spec.rb +349 -0
  39. data/spec/models/spree_gateway_affirm_spec.rb +137 -0
  40. data/spec/spec_helper.rb +98 -0
  41. data/spree_affirm.gemspec +30 -0
  42. metadata +53 -7
@@ -0,0 +1,21 @@
1
+ module SpreeAffirm
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ class_option :auto_run_migrations, :type => :boolean, :default => false
6
+
7
+ def add_migrations
8
+ run 'bundle exec rake railties:install:migrations FROM=spree_affirm'
9
+ end
10
+
11
+ def run_migrations
12
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
13
+ if run_migrations
14
+ run 'bundle exec rake db:migrate'
15
+ else
16
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ require 'spree_core'
2
+ require 'spree_affirm/engine'
@@ -0,0 +1,27 @@
1
+ module SpreeAffirm
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'spree_affirm'
6
+
7
+
8
+ # use rspec for tests
9
+ config.generators do |g|
10
+ g.test_framework :rspec
11
+ end
12
+
13
+ config.autoload_paths += %W(#{config.root}/lib)
14
+
15
+ def self.activate
16
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
17
+ Rails.configuration.cache_classes ? require(c) : load(c)
18
+ end
19
+ end
20
+
21
+ config.to_prepare &method(:activate).to_proc
22
+
23
+ initializer "spree.spree_affirm.payment_methods", :after => "spree.register.payment_methods" do |app|
24
+ app.config.spree.payment_methods << Spree::Gateway::Affirm
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ require 'factory_girl'
2
+
3
+ Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
4
+ require File.expand_path(f)
5
+ end
@@ -0,0 +1,219 @@
1
+ def BASE_CHECKOUT_DETAILS
2
+ {
3
+ "merchant"=> {
4
+ "public_api_key"=> "PPPPPPPPPPPPPPP",
5
+ "user_cancel_url"=> "http=>//google.com/cancel",
6
+ "user_confirmation_url"=> "http=>//google.com/confirm",
7
+ "name"=> "Test Merchant"
8
+ },
9
+ "tax_amount"=> 0,
10
+ "billing"=> {
11
+ "address"=> {
12
+ "city"=> "San Francisco",
13
+ "street1"=> "12345 Main",
14
+ "street2"=> "300",
15
+ "region1_code"=> "AL",
16
+ "postal_code"=> "55555",
17
+ "country_code"=> "USA",
18
+ "for_billing"=> true,
19
+ "validation_source"=> 3
20
+ },
21
+ "email"=> "test@affirm.com",
22
+ "name"=> {
23
+ "for_billing"=> true,
24
+ "last"=> "Doe",
25
+ "first"=> "John"
26
+ }
27
+ },
28
+ "items"=> {
29
+ "xxx-xx-xxx-x"=> {
30
+ "sku"=> "xxx-xx-xxx-x",
31
+ "item_url"=> "http=>//google.com/products/the-blue-hat",
32
+ "display_name"=> "The Blue Hat",
33
+ "unit_price"=> 85000,
34
+ "qty"=> 1,
35
+ "item_type"=> "physical",
36
+ "item_image_url"=> "http=>//google.com/products/6/large/the-blue-hat"
37
+ }
38
+ },
39
+ "shipping"=> {
40
+ "name"=> {
41
+ "last"=> "Doe",
42
+ "first"=> "John"
43
+ },
44
+ "address"=> {
45
+ "city"=> "San Francisco",
46
+ "street1"=> "12345 Main Street",
47
+ "street2"=> "300",
48
+ "region1_code"=> "AL",
49
+ "postal_code"=> "94110",
50
+ "country_code"=> "USA",
51
+ "validation_source"=> 3
52
+ }
53
+ },
54
+ "checkout_id"=> "S123123-456",
55
+ "currency"=> "USD",
56
+ "meta"=> {
57
+ "release"=> "true",
58
+ "user_timezone"=> "America/Los_Angeles",
59
+ "__affirm_tracking_uuid"=> "97570a41-cd07-4f52-8869-46c6d2588407"
60
+ },
61
+ "discount_code"=> "",
62
+ "misc_fee_amount"=> 0,
63
+ "shipping_type"=> "Free National UPS",
64
+ "config"=> {
65
+ "financial_product_key"=> "XXXXXXXXXXXXXXX",
66
+ "financial_product_type"=> "splitpay",
67
+ "required_billing_fields"=> [
68
+ "name",
69
+ "address",
70
+ "email"
71
+ ]
72
+ },
73
+ "api_version"=> "v2",
74
+ "shipping_amount"=> 0
75
+ }
76
+ end
77
+
78
+ FactoryGirl.define do
79
+ factory :affirm_checkout, class: Spree::AffirmCheckout do
80
+ token "12345678910"
81
+ association(:payment_method, factory: :affirm_payment_method)
82
+ association(:order, factory: :order_with_line_items)
83
+
84
+
85
+ ignore do
86
+ stub_details true
87
+ product_key_mismatch false
88
+ shipping_address_mismatch false
89
+ billing_address_mismatch false
90
+ alternate_billing_address_format false
91
+ billing_address_full_name false
92
+ billing_email_mismatch false
93
+ extra_line_item false
94
+ missing_line_item false
95
+ quantity_mismatch false
96
+ price_mismatch false
97
+ full_name_case_mismatch false
98
+ end
99
+
100
+ after(:build) do |checkout, evaluator|
101
+
102
+ _details = BASE_CHECKOUT_DETAILS()
103
+
104
+ # product keys
105
+ unless evaluator.product_key_mismatch
106
+ _details['config']['financial_product_key'] = checkout.payment_method.preferred_product_key
107
+ end
108
+
109
+ # case mismatch
110
+ unless evaluator.full_name_case_mismatch
111
+ _details['billing']['name'] = {
112
+ "full" => checkout.order.bill_address.firstname.upcase + " " +
113
+ checkout.order.bill_address.lastname.upcase
114
+ }
115
+ end
116
+
117
+ # shipping address
118
+ unless evaluator.shipping_address_mismatch
119
+ _details['shipping'] = {
120
+ "name" => {
121
+ "first" => checkout.order.ship_address.firstname,
122
+ "last" => checkout.order.ship_address.lastname
123
+ },
124
+ "address"=> {
125
+ "city"=> checkout.order.ship_address.city,
126
+ "street1"=> checkout.order.ship_address.address1,
127
+ "street2"=> checkout.order.ship_address.address2,
128
+ "region1_code"=> checkout.order.ship_address.state.abbr,
129
+ "postal_code"=> checkout.order.ship_address.zipcode,
130
+ "country_code"=> checkout.order.ship_address.country.iso3
131
+ }
132
+ }
133
+ end
134
+
135
+ # billing address
136
+ unless evaluator.billing_address_mismatch
137
+ _details["billing"] = {
138
+ "email" => "joe@schmoe.com",
139
+ "name" => {
140
+ "first" => checkout.order.bill_address.firstname,
141
+ "last" => checkout.order.bill_address.lastname
142
+ },
143
+ "address"=> {
144
+ "city"=> checkout.order.bill_address.city,
145
+ "street1"=> checkout.order.bill_address.address1,
146
+ "street2"=> checkout.order.bill_address.address2,
147
+ "region1_code"=> checkout.order.bill_address.state.abbr,
148
+ "postal_code"=> checkout.order.bill_address.zipcode,
149
+ "country_code"=> checkout.order.bill_address.country.iso3
150
+ }
151
+ }
152
+ end
153
+
154
+ # use alternate format for billing address
155
+ if evaluator.alternate_billing_address_format
156
+ _details['billing']["address"] = {
157
+ "city" => _details['billing']["address"]["city"],
158
+ "line1"=> _details['billing']["address"]["street1"],
159
+ "line2"=> _details['billing']["address"]["street2"],
160
+ "state"=> _details['billing']["address"]["postal_code"],
161
+ "zipcode"=> _details['billing']["address"]["region1_code"],
162
+ "country"=> _details['billing']["address"]["country_code"]
163
+ }
164
+ end
165
+
166
+ # use name.full instead of first/last
167
+ if evaluator.billing_address_full_name
168
+ _details['billing']['name'] = {
169
+ 'full' => "#{_details['billing']['name']['first']} #{_details['billing']['name']['last']}"
170
+ }
171
+ end
172
+
173
+
174
+ # billing email
175
+ unless evaluator.billing_email_mismatch
176
+ _details["billing"]["email"] = checkout.order.email
177
+ end
178
+
179
+ # setup items in cart
180
+ _details['items'] = {}
181
+ checkout.order.line_items.each do |item|
182
+ _details['items'][item.variant.sku] = {
183
+ "qty" => item.quantity.to_s,
184
+ "unit_price" => (item.price*100).to_s,
185
+ "display_name" => item.product.name
186
+ }
187
+ end
188
+
189
+ if evaluator.extra_line_item
190
+ _details['items']['extra-1-2-3'] = {
191
+ "qty" => "1",
192
+ "unit_price" => "12300",
193
+ "display_name" => "Really cool hat"
194
+ }
195
+ end
196
+
197
+ if evaluator.missing_line_item
198
+ _details['items'].delete _details['items'].keys.last
199
+ end
200
+
201
+ if evaluator.quantity_mismatch
202
+ _last_item = _details['items'][_details['items'].keys.last]
203
+ _details['items'][_details['items'].keys.last]['qty'] = (_last_item['qty'].to_i + 1).to_s
204
+ end
205
+
206
+ if evaluator.price_mismatch
207
+ _last_item = _details['items'][_details['items'].keys.last]
208
+ _details['items'][_details['items'].keys.last]['unit_price'] = 456456
209
+ end
210
+
211
+ if evaluator.stub_details
212
+ checkout.stub(details: _details)
213
+ end
214
+ end
215
+
216
+ end
217
+ end
218
+
219
+
@@ -0,0 +1,8 @@
1
+ FactoryGirl.define do
2
+ factory :affirm_payment_method, class: Spree::Gateway::Affirm do
3
+ name "Staging Affirm Split Pay"
4
+ active true
5
+ environment "test"
6
+ auto_capture false
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ FactoryGirl.define do
2
+ factory :affirm_payment, class: Spree::Payment do
3
+ amount 45.75
4
+ association(:payment_method, factory: :affirm_payment_method)
5
+ association(:source, factory: :affirm_checkout)
6
+ order
7
+ state 'checkout'
8
+ response_code '12345'
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module SpreeAffirm
2
+ VERSION = "0.2.20"
3
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::AffirmController do
4
+ let(:user) { FactoryGirl.create(:user) }
5
+ let(:checkout) { FactoryGirl.build(:affirm_checkout) }
6
+ let(:bad_billing_checkout) { FactoryGirl.build(:affirm_checkout, billing_address_mismatch: true) }
7
+ let(:bad_shipping_checkout) { FactoryGirl.build(:affirm_checkout, shipping_address_mismatch: true) }
8
+ let(:bad_email_checkout) { FactoryGirl.build(:affirm_checkout, billing_email_mismatch: true) }
9
+
10
+
11
+ describe "POST confirm" do
12
+
13
+ def post_request(token, payment_id)
14
+ post :confirm, checkout_token: token, payment_method_id: payment_id, use_route: 'spree'
15
+ end
16
+
17
+ before do
18
+ controller.stub authenticate_spree_user!: true
19
+ controller.stub spree_current_user: user
20
+ end
21
+
22
+ context "when the checkout matches the order" do
23
+ before do
24
+ Spree::AffirmCheckout.stub new: checkout
25
+ controller.stub current_order: checkout.order
26
+ end
27
+
28
+ context "when no checkout_token is provided" do
29
+ it "redirects to the current order state" do
30
+ post_request(nil, nil)
31
+ expect(response).to redirect_to(controller.checkout_state_path(checkout.order.state))
32
+ end
33
+ end
34
+
35
+ context "when the order is complete" do
36
+ before do
37
+ checkout.order.state = 'complete'
38
+ end
39
+ it "redirects to the current order state" do
40
+ post_request '123456789', checkout.payment_method.id
41
+ expect(response).to redirect_to(controller.order_path(checkout.order))
42
+ end
43
+ end
44
+
45
+ context "when the order state is payment" do
46
+ before do
47
+ checkout.order.state = 'payment'
48
+ end
49
+
50
+ it "creates a new payment" do
51
+ post_request "123423423", checkout.payment_method.id
52
+
53
+ expect(checkout.order.payments.first.source).to eq(checkout)
54
+ end
55
+
56
+ # it "transitions to complete if confirmation is not required" do
57
+ # checkout.order.stub confirmation_required?: false
58
+ # post_request "123423423", checkout.payment_method.id
59
+
60
+ # expect(checkout.order.state).to eq("complete")
61
+ # end
62
+
63
+ it "transitions to confirm if confirmation is required" do
64
+ checkout.order.stub confirmation_required?: true
65
+ post_request "123423423", checkout.payment_method.id
66
+
67
+ expect(checkout.order.reload.state).to eq("confirm")
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ context "when the billing_address does not match the order" do
74
+ before do
75
+ Spree::AffirmCheckout.stub new: bad_billing_checkout
76
+ state = FactoryGirl.create(:state, abbr: bad_billing_checkout.details['billing']['address']['region1_code'])
77
+ Spree::State.stub find_by_abbr: state, find_by_name: state
78
+ controller.stub current_order: bad_billing_checkout.order
79
+ end
80
+
81
+ it "creates a new address record for the order" do
82
+ _old_billing_address = bad_billing_checkout.order.bill_address
83
+ post_request '12345789', bad_billing_checkout.payment_method.id
84
+
85
+ expect(bad_billing_checkout.order.bill_address).not_to be(_old_billing_address)
86
+ expect(bad_billing_checkout.valid?).to be(true)
87
+ end
88
+ end
89
+
90
+
91
+ context "when the shipping_address does not match the order" do
92
+ before do
93
+ Spree::AffirmCheckout.stub new: bad_shipping_checkout
94
+ state = FactoryGirl.create(:state, abbr: bad_shipping_checkout.details['shipping']['address']['region1_code'])
95
+ Spree::State.stub find_by_abbr: state, find_by_name: state
96
+ controller.stub current_order: bad_shipping_checkout.order
97
+ end
98
+
99
+ it "creates a new address record for the order" do
100
+ _old_shipping_address = bad_shipping_checkout.order.ship_address
101
+ post_request '12345789', bad_shipping_checkout.payment_method.id
102
+
103
+ expect(bad_shipping_checkout.order.ship_address).not_to be(_old_shipping_address)
104
+ expect(bad_shipping_checkout.valid?).to be(true)
105
+ end
106
+ end
107
+
108
+
109
+
110
+ context "when the billing_email does not match the order" do
111
+ before do
112
+ Spree::AffirmCheckout.stub new: bad_email_checkout
113
+ controller.stub current_order: bad_email_checkout.order
114
+ end
115
+
116
+ it "updates the billing_email on the order" do
117
+ _old_email = bad_email_checkout.order.email
118
+ post_request '12345789', bad_email_checkout.payment_method.id
119
+
120
+ expect(bad_email_checkout.order.email).not_to be(_old_email)
121
+ expect(bad_email_checkout.valid?).to be(true)
122
+ end
123
+ end
124
+
125
+
126
+ context "there is no current order" do
127
+ before(:each) do
128
+ controller.stub current_order: nil
129
+ end
130
+
131
+ it "raises an ActiveRecord::RecordNotFound error" do
132
+ expect do
133
+ post_request nil, nil
134
+ end.to raise_error(ActiveRecord::RecordNotFound)
135
+ end
136
+ end
137
+ end
138
+ end