solidus_afterpay 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +41 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +20 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +14 -0
  9. data/CHANGELOG.md +1 -0
  10. data/Gemfile +33 -0
  11. data/LICENSE +202 -0
  12. data/README.md +175 -0
  13. data/Rakefile +6 -0
  14. data/app/assets/javascripts/solidus_afterpay/afterpay_checkout.js +131 -0
  15. data/app/assets/javascripts/spree/backend/solidus_afterpay.js +2 -0
  16. data/app/assets/javascripts/spree/frontend/solidus_afterpay.js +4 -0
  17. data/app/assets/stylesheets/spree/backend/solidus_afterpay.css +4 -0
  18. data/app/assets/stylesheets/spree/frontend/solidus_afterpay.css +4 -0
  19. data/app/controllers/solidus_afterpay/base_controller.rb +30 -0
  20. data/app/controllers/solidus_afterpay/callbacks_controller.rb +71 -0
  21. data/app/controllers/solidus_afterpay/checkouts_controller.rb +47 -0
  22. data/app/decorators/controllers/solidus_afterpay/spree/checkout_controller_decorator.rb +13 -0
  23. data/app/decorators/models/solidus_afterpay/spree/order_decorator.rb +15 -0
  24. data/app/helpers/solidus_afterpay/afterpay_helper.rb +15 -0
  25. data/app/models/solidus_afterpay/gateway.rb +157 -0
  26. data/app/models/solidus_afterpay/order_component_builder.rb +97 -0
  27. data/app/models/solidus_afterpay/payment_method.rb +56 -0
  28. data/app/models/solidus_afterpay/payment_source.rb +23 -0
  29. data/app/models/solidus_afterpay/user_agent_generator.rb +35 -0
  30. data/app/views/solidus_afterpay/_afterpay_javascript.html.erb +5 -0
  31. data/app/views/spree/admin/payments/source_forms/_afterpay.html.erb +1 -0
  32. data/app/views/spree/admin/payments/source_views/_afterpay.html.erb +0 -0
  33. data/app/views/spree/api/payments/source_views/_afterpay.json.jbuilder +3 -0
  34. data/app/views/spree/checkout/payment/_afterpay.html.erb +9 -0
  35. data/app/views/spree/shared/_afterpay_messaging.html.erb +13 -0
  36. data/bin/console +17 -0
  37. data/bin/rails +7 -0
  38. data/bin/rails-engine +13 -0
  39. data/bin/rails-sandbox +16 -0
  40. data/bin/rake +7 -0
  41. data/bin/sandbox +86 -0
  42. data/bin/setup +8 -0
  43. data/codecov.yml +2 -0
  44. data/config/locales/en.yml +12 -0
  45. data/config/routes.rb +7 -0
  46. data/db/migrate/20210813142725_create_solidus_afterpay_payment_sources.rb +12 -0
  47. data/lib/generators/solidus_afterpay/install/install_generator.rb +43 -0
  48. data/lib/generators/solidus_afterpay/install/templates/initializer.rb +5 -0
  49. data/lib/solidus_afterpay/configuration.rb +25 -0
  50. data/lib/solidus_afterpay/engine.rb +25 -0
  51. data/lib/solidus_afterpay/testing_support/factories.rb +20 -0
  52. data/lib/solidus_afterpay/version.rb +5 -0
  53. data/lib/solidus_afterpay.rb +5 -0
  54. data/solidus_afterpay.gemspec +38 -0
  55. data/spec/fixtures/vcr_casettes/create_checkout/invalid.yml +65 -0
  56. data/spec/fixtures/vcr_casettes/create_checkout/valid.yml +64 -0
  57. data/spec/fixtures/vcr_casettes/credit/invalid.yml +61 -0
  58. data/spec/fixtures/vcr_casettes/credit/valid.yml +63 -0
  59. data/spec/fixtures/vcr_casettes/deferred/authorize/declined_payment.yml +120 -0
  60. data/spec/fixtures/vcr_casettes/deferred/authorize/invalid.yml +61 -0
  61. data/spec/fixtures/vcr_casettes/deferred/authorize/valid.yml +120 -0
  62. data/spec/fixtures/vcr_casettes/deferred/capture/invalid.yml +61 -0
  63. data/spec/fixtures/vcr_casettes/deferred/capture/valid.yml +140 -0
  64. data/spec/fixtures/vcr_casettes/deferred/void/invalid.yml +61 -0
  65. data/spec/fixtures/vcr_casettes/deferred/void/valid.yml +137 -0
  66. data/spec/fixtures/vcr_casettes/find_payment/invalid.yml +61 -0
  67. data/spec/fixtures/vcr_casettes/find_payment/valid.yml +140 -0
  68. data/spec/fixtures/vcr_casettes/immediate/capture/declined_payment.yml +120 -0
  69. data/spec/fixtures/vcr_casettes/immediate/capture/invalid.yml +61 -0
  70. data/spec/fixtures/vcr_casettes/immediate/capture/valid.yml +134 -0
  71. data/spec/fixtures/vcr_casettes/retrieve_configuration/valid.yml +67 -0
  72. data/spec/helpers/solidus_afterpay/afterpay_helper_spec.rb +23 -0
  73. data/spec/models/solidus_afterpay/gateway_spec.rb +418 -0
  74. data/spec/models/solidus_afterpay/order_component_builder_spec.rb +137 -0
  75. data/spec/models/solidus_afterpay/payment_method_spec.rb +143 -0
  76. data/spec/models/solidus_afterpay/payment_source_spec.rb +61 -0
  77. data/spec/models/solidus_afterpay/user_agent_generator_spec.rb +22 -0
  78. data/spec/models/spree/order_spec.rb +158 -0
  79. data/spec/requests/solidus_afterpay/callbacks_controller_spec.rb +127 -0
  80. data/spec/requests/solidus_afterpay/checkouts_controller_spec.rb +190 -0
  81. data/spec/spec_helper.rb +31 -0
  82. data/spec/support/auth.rb +15 -0
  83. data/spec/support/preferences.rb +33 -0
  84. data/spec/support/vcr.rb +18 -0
  85. metadata +249 -0
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusAfterpay::OrderComponentBuilder do
4
+ let(:order) { build(:order_with_line_items) }
5
+ let(:redirect_confirm_url) { 'https://merchantsite.com/confirm' }
6
+ let(:redirect_cancel_url) { 'https://merchantsite.com/cancel' }
7
+
8
+ let(:builder) do
9
+ described_class.new(
10
+ order: order,
11
+ redirect_confirm_url: redirect_confirm_url,
12
+ redirect_cancel_url: redirect_cancel_url
13
+ )
14
+ end
15
+
16
+ describe '#call' do
17
+ subject(:result) { builder.call }
18
+
19
+ let(:expected_result_not_combined) do
20
+ Afterpay::Components::Order.new(
21
+ amount: Afterpay::Components::Money.new(amount: '110.0', currency: 'USD'),
22
+ billing: Afterpay::Components::Contact.new(
23
+ area1: 'Herndon',
24
+ area2: nil,
25
+ country_code: nil,
26
+ line1: 'PO Box 1337',
27
+ line2: 'Northwest',
28
+ name: 'John',
29
+ phone_number: '555-555-0199',
30
+ postcode: order.billing_address.zipcode,
31
+ region: 'AL'
32
+ ),
33
+ consumer: Afterpay::Components::Consumer.new(
34
+ email: order.user.email,
35
+ given_names: 'John',
36
+ phone_number: nil,
37
+ surname: nil
38
+ ),
39
+ courier: nil,
40
+ discounts: nil,
41
+ items: [
42
+ Afterpay::Components::Item.new(
43
+ name: order.line_items.first.name,
44
+ price: Afterpay::Components::Money.new(
45
+ amount: '10.0',
46
+ currency: 'USD'
47
+ ),
48
+ quantity: 1,
49
+ sku: order.line_items.first.sku
50
+ )
51
+ ],
52
+ merchant: Afterpay::Components::Merchant.new(
53
+ redirect_confirm_url: 'https://merchantsite.com/confirm',
54
+ redirect_cancel_url: 'https://merchantsite.com/cancel'
55
+ ),
56
+ merchant_reference: order.number,
57
+ payment_type: nil,
58
+ shipping: Afterpay::Components::Contact.new(
59
+ area1: 'Herndon',
60
+ area2: nil,
61
+ country_code: nil,
62
+ line1: 'A Different Road',
63
+ line2: 'Northwest',
64
+ name: 'John',
65
+ phone_number: '555-555-0199',
66
+ postcode: order.shipping_address.zipcode,
67
+ region: 'AL'
68
+ ),
69
+ shipping_amount: nil,
70
+ tax_amount: nil
71
+ )
72
+ end
73
+
74
+ let(:expected_result_combined) do
75
+ Afterpay::Components::Order.new(
76
+ amount: Afterpay::Components::Money.new(amount: '110.0', currency: 'USD'),
77
+ billing: Afterpay::Components::Contact.new(
78
+ area1: 'Herndon',
79
+ area2: nil,
80
+ country_code: nil,
81
+ line1: 'PO Box 1337',
82
+ line2: 'Northwest',
83
+ name: 'John Von Doe',
84
+ phone_number: '555-555-0199',
85
+ postcode: order.billing_address.zipcode,
86
+ region: 'AL'
87
+ ),
88
+ consumer: Afterpay::Components::Consumer.new(
89
+ email: order.user.email,
90
+ given_names: 'John',
91
+ phone_number: nil,
92
+ surname: 'Von Doe'
93
+ ),
94
+ courier: nil,
95
+ discounts: nil,
96
+ items: [
97
+ Afterpay::Components::Item.new(
98
+ name: order.line_items.first.name,
99
+ price: Afterpay::Components::Money.new(
100
+ amount: '10.0',
101
+ currency: 'USD'
102
+ ),
103
+ quantity: 1,
104
+ sku: order.line_items.first.sku
105
+ )
106
+ ],
107
+ merchant: Afterpay::Components::Merchant.new(
108
+ redirect_confirm_url: 'https://merchantsite.com/confirm',
109
+ redirect_cancel_url: 'https://merchantsite.com/cancel'
110
+ ),
111
+ merchant_reference: order.number,
112
+ payment_type: nil,
113
+ shipping: Afterpay::Components::Contact.new(
114
+ area1: 'Herndon',
115
+ area2: nil,
116
+ country_code: nil,
117
+ line1: 'A Different Road',
118
+ line2: 'Northwest',
119
+ name: 'John Von Doe',
120
+ phone_number: '555-555-0199',
121
+ postcode: order.shipping_address.zipcode,
122
+ region: 'AL'
123
+ ),
124
+ shipping_amount: nil,
125
+ tax_amount: nil
126
+ )
127
+ end
128
+
129
+ let(:expected_result) do
130
+ SolidusSupport.combined_first_and_last_name_in_address? ? expected_result_combined : expected_result_not_combined
131
+ end
132
+
133
+ it 'returns the correct payload' do
134
+ expect(result.as_json).to eq(expected_result.as_json)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusAfterpay::PaymentMethod, type: :model do
4
+ let(:payment_method) { described_class.new }
5
+
6
+ describe "#gateway_class" do
7
+ subject { payment_method.gateway_class }
8
+
9
+ it { is_expected.to eq(SolidusAfterpay::Gateway) }
10
+ end
11
+
12
+ describe "#payment_source_class" do
13
+ subject { payment_method.payment_source_class }
14
+
15
+ it { is_expected.to eq(SolidusAfterpay::PaymentSource) }
16
+ end
17
+
18
+ describe "#partial_name" do
19
+ subject { payment_method.partial_name }
20
+
21
+ it { is_expected.to eq('afterpay') }
22
+ end
23
+
24
+ describe "#try_void" do
25
+ subject { payment_method.try_void(payment) }
26
+
27
+ let(:response_code) { 'RESPONSE_CODE' }
28
+ let(:payment_source) { build(:afterpay_payment_source) }
29
+ let(:payment) { build(:afterpay_payment, response_code: response_code, source: payment_source) }
30
+
31
+ let(:can_void?) { true }
32
+ let(:gateway_response_success?) { true }
33
+ let(:gateway_response) { ActiveMerchant::Billing::Response.new(gateway_response_success?, '') }
34
+
35
+ let(:gateway_options) { { originator: payment, currency: 'USD' } }
36
+
37
+ before do
38
+ allow(payment_source).to receive(:can_void?).and_return(can_void?)
39
+ allow(payment_method).to receive(:void).with(response_code, gateway_options).and_return(gateway_response)
40
+ end
41
+
42
+ context 'when the void completes successful' do
43
+ it 'returns the void response' do
44
+ is_expected.to eq(gateway_response)
45
+ end
46
+ end
47
+
48
+ context 'when the void throws an error' do
49
+ let(:gateway_response_success?) { false }
50
+
51
+ it { is_expected.to be(false) }
52
+ end
53
+
54
+ context "when the payment can't be voided" do
55
+ let(:can_void?) { false }
56
+
57
+ it { is_expected.to be(false) }
58
+ end
59
+ end
60
+
61
+ describe "#available_for_order?" do
62
+ subject { payment_method.available_for_order?(order) }
63
+
64
+ let(:preferred_minimum_amount) { nil }
65
+ let(:preferred_maximum_amount) { nil }
66
+ let(:preferred_currency) { nil }
67
+
68
+ let(:payment_method) do
69
+ described_class.new(
70
+ preferred_minimum_amount: preferred_minimum_amount,
71
+ preferred_maximum_amount: preferred_maximum_amount,
72
+ preferred_currency: preferred_currency
73
+ )
74
+ end
75
+
76
+ let(:order) { build(:order, currency: order_currency, total: order_total) }
77
+ let(:order_total) { 5 }
78
+ let(:order_currency) { 'USD' }
79
+
80
+ let(:configuration) do
81
+ require 'hashie'
82
+
83
+ Hashie::Mash.new({
84
+ minimumAmount: { amount: '1', currency: 'USD' },
85
+ maximumAmount: { amount: '10', currency: 'USD' }
86
+ })
87
+ end
88
+
89
+ before do
90
+ allow(payment_method.gateway).to receive(:retrieve_configuration).and_return(configuration)
91
+ end
92
+
93
+ context 'when preference settings are nil' do
94
+ context 'when order total is inside the range' do
95
+ it { is_expected.to be(true) }
96
+ end
97
+
98
+ context 'when order total is outside the range' do
99
+ let(:order_total) { 11 }
100
+
101
+ it { is_expected.to be(false) }
102
+ end
103
+
104
+ context 'when order currency is different from afterpay configuration' do
105
+ let(:order_currency) { 'EUR' }
106
+
107
+ it { is_expected.to be(false) }
108
+ end
109
+
110
+ context "when afterpay configuration doesn't include the minumumAmount" do
111
+ let(:configuration) do
112
+ require 'hashie'
113
+
114
+ Hashie::Mash.new({ maximumAmount: { amount: '10', currency: 'USD' } })
115
+ end
116
+
117
+ it { is_expected.to be(true) }
118
+ end
119
+ end
120
+
121
+ context 'when preference settings are not nil' do
122
+ let(:preferred_minimum_amount) { 1 }
123
+ let(:preferred_maximum_amount) { 10 }
124
+ let(:preferred_currency) { 'USD' }
125
+
126
+ context 'when order total is inside the range' do
127
+ it { is_expected.to be(true) }
128
+ end
129
+
130
+ context 'when order total is outside the range' do
131
+ let(:order_total) { 11 }
132
+
133
+ it { is_expected.to be(false) }
134
+ end
135
+
136
+ context 'when order currency is different from afterpay configuration' do
137
+ let(:order_currency) { 'EUR' }
138
+
139
+ it { is_expected.to be(false) }
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusAfterpay::PaymentSource, type: :model do
4
+ let(:payment_source) { described_class.new }
5
+
6
+ describe '#actions' do
7
+ subject { payment_source.actions }
8
+
9
+ it 'supports capture, void, and credit' do
10
+ is_expected.to eq(%w[capture void credit])
11
+ end
12
+ end
13
+
14
+ describe '#can_void?' do
15
+ subject { payment_source.can_void?(payment) }
16
+
17
+ let(:deferred?) { false }
18
+ let(:payment_method) { build(:afterpay_payment_method, preferred_deferred: deferred?) }
19
+ let(:payment) { build(:afterpay_payment, payment_method: payment_method) }
20
+
21
+ context 'with the immediate flow' do
22
+ it 'is always false' do
23
+ is_expected.to be(false)
24
+ end
25
+ end
26
+
27
+ context 'with the deferred flow' do
28
+ let(:deferred?) { true }
29
+
30
+ let(:payment_state) { 'AUTH_APPROVED' }
31
+ let(:gateway_response) { { paymentState: payment_state } }
32
+ let(:gateway) { instance_double(SolidusAfterpay::Gateway, find_payment: gateway_response) }
33
+
34
+ before do
35
+ allow(payment_method).to receive(:gateway).and_return(gateway)
36
+ end
37
+
38
+ context 'when the payment exists and the payment state is voidable' do
39
+ it 'returns true' do
40
+ is_expected.to be(true)
41
+ end
42
+ end
43
+
44
+ context 'when the payment exists when the payment state is not voidable' do
45
+ let(:payment_state) { 'NOT_VOIDABLE' }
46
+
47
+ it 'returns false' do
48
+ is_expected.to be(false)
49
+ end
50
+ end
51
+
52
+ context "when the payment doesn't exist" do
53
+ let(:gateway_response) { nil }
54
+
55
+ it 'returns false' do
56
+ is_expected.to be(false)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusAfterpay::UserAgentGenerator do
4
+ describe '#generate' do
5
+ subject { user_agent_generator.generate }
6
+
7
+ let(:user_agent_generator) { described_class.new(merchant_id: merchant_id) }
8
+ let(:merchant_id) { 'MERCHANT_ID' }
9
+ let(:default_store) { build(:store, url: 'test.com') }
10
+
11
+ before do
12
+ stub_const('SolidusAfterpay::VERSION', '0.1.0')
13
+ allow(::Spree).to receive(:solidus_gem_version).and_return('3.0.1')
14
+ stub_const('RUBY_VERSION', '2.6.6')
15
+ allow(::Spree::Store).to receive(:default).and_return(default_store)
16
+ end
17
+
18
+ it 'includes the production javascript' do
19
+ is_expected.to eq('SolidusAfterpay/0.1.0 (Solidus/3.0.1; Ruby/2.6.6; Merchant/MERCHANT_ID) https://test.com')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Spree::Order, type: :model do
6
+ let(:store) { create(:store) }
7
+ let(:user) { create(:user, email: "solidus@example.com") }
8
+ let(:order) { create(:order, user: user, store: store) }
9
+
10
+ describe "#available_payment_methods" do
11
+ it "includes frontend payment methods" do
12
+ payment_method = Spree::PaymentMethod::Check.create!({
13
+ name: "Fake",
14
+ active: true,
15
+ available_to_users: true,
16
+ available_to_admin: false
17
+ })
18
+ expect(order.available_payment_methods).to include(payment_method)
19
+ end
20
+
21
+ it "includes 'both' payment methods" do
22
+ payment_method = Spree::PaymentMethod::Check.create!({
23
+ name: "Fake",
24
+ active: true,
25
+ available_to_users: true,
26
+ available_to_admin: true
27
+ })
28
+ expect(order.available_payment_methods).to include(payment_method)
29
+ end
30
+
31
+ # rubocop:disable RSpec/MultipleExpectations
32
+ it "does not include a payment method twice" do
33
+ payment_method = Spree::PaymentMethod::Check.create!({
34
+ name: "Fake",
35
+ active: true,
36
+ available_to_users: true,
37
+ available_to_admin: true
38
+ })
39
+ expect(order.available_payment_methods.count).to eq(1)
40
+ expect(order.available_payment_methods).to include(payment_method)
41
+ end
42
+ # rubocop:enable RSpec/MultipleExpectations
43
+
44
+ it "does not include inactive payment methods" do
45
+ Spree::PaymentMethod::Check.create!({
46
+ name: "Fake",
47
+ active: false,
48
+ available_to_users: true,
49
+ available_to_admin: true
50
+ })
51
+ expect(order.available_payment_methods.count).to eq(0)
52
+ end
53
+
54
+ context "with more than one payment method" do
55
+ subject { order.available_payment_methods }
56
+
57
+ let!(:first_method) {
58
+ FactoryBot.create(:payment_method, available_to_users: true,
59
+ available_to_admin: true)
60
+ }
61
+ let!(:second_method) {
62
+ FactoryBot.create(:payment_method, available_to_users: true,
63
+ available_to_admin: true)
64
+ }
65
+
66
+ before do
67
+ second_method.move_to_top
68
+ end
69
+
70
+ it "respects the order of methods based on position" do
71
+ is_expected.to eq([second_method, first_method])
72
+ end
73
+
74
+ context 'when a payment method responds to #available_for_order?' do
75
+ let(:third_method) {
76
+ FakePaymentMethod.create(name: 'Fake', available_to_users: true, available_to_admin: true)
77
+ }
78
+
79
+ before do
80
+ fake_payment_method_class = Class.new(SolidusSupport.payment_method_parent_class)
81
+ stub_const('FakePaymentMethod', fake_payment_method_class)
82
+
83
+ third_method
84
+ end
85
+
86
+ context 'when it responds with true' do
87
+ before do
88
+ FakePaymentMethod.class_eval { def available_for_order?(_order); true; end }
89
+ end
90
+
91
+ it 'includes it in the result' do
92
+ is_expected.to eq([second_method, first_method, third_method])
93
+ end
94
+ end
95
+
96
+ context 'when it responds with false' do
97
+ before do
98
+ FakePaymentMethod.class_eval { def available_for_order?(_order); false; end }
99
+ end
100
+
101
+ it "doesn't include it in the result" do
102
+ is_expected.to eq([second_method, first_method])
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ context 'when the order has a store' do
109
+ let(:order) { create(:order) }
110
+
111
+ let!(:store_with_payment_methods) do
112
+ create(:store,
113
+ payment_methods: [payment_method_with_store])
114
+ end
115
+ let!(:payment_method_with_store) { create(:payment_method) }
116
+ let!(:store_without_payment_methods) { create(:store) }
117
+ let!(:payment_method_without_store) { create(:payment_method) }
118
+
119
+ context 'when the store has payment methods' do
120
+ before { order.update!(store: store_with_payment_methods) }
121
+
122
+ it 'returns only the matching payment methods for that store' do
123
+ expect(order.available_payment_methods).to match_array(
124
+ [payment_method_with_store]
125
+ )
126
+ end
127
+
128
+ context 'when the store has an extra payment method unavailable to users' do
129
+ let!(:admin_only_payment_method) do
130
+ create(:payment_method,
131
+ available_to_users: false,
132
+ available_to_admin: true)
133
+ end
134
+
135
+ before do
136
+ store_with_payment_methods.payment_methods << admin_only_payment_method
137
+ end
138
+
139
+ it 'returns only the payment methods available to users for that store' do
140
+ expect(order.available_payment_methods).to match_array(
141
+ [payment_method_with_store]
142
+ )
143
+ end
144
+ end
145
+ end
146
+
147
+ context 'when the store does not have payment methods' do
148
+ before { order.update!(store: store_without_payment_methods) }
149
+
150
+ it 'returns all matching payment methods regardless of store' do
151
+ expect(order.available_payment_methods).to match_array(
152
+ [payment_method_with_store, payment_method_without_store]
153
+ )
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end