solidus_afterpay 0.1.0

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 (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