solidus_stripe 4.4.0 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +102 -41
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +94 -4
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +1 -265
  7. data/Gemfile +10 -30
  8. data/LICENSE +2 -2
  9. data/Procfile.dev +3 -0
  10. data/README.md +185 -213
  11. data/Rakefile +7 -6
  12. data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
  13. data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
  14. data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
  15. data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
  16. data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
  17. data/app/models/solidus_stripe/customer.rb +21 -0
  18. data/app/models/solidus_stripe/gateway.rb +229 -0
  19. data/app/models/solidus_stripe/payment_intent.rb +124 -0
  20. data/app/models/solidus_stripe/payment_method.rb +106 -0
  21. data/app/models/solidus_stripe/payment_source.rb +31 -0
  22. data/app/models/solidus_stripe/slug_entry.rb +20 -0
  23. data/app/models/solidus_stripe.rb +7 -0
  24. data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
  25. data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
  26. data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
  27. data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +19 -0
  28. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
  29. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
  30. data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
  31. data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
  32. data/bin/dev +13 -0
  33. data/bin/dummy-app +29 -0
  34. data/bin/rails +38 -3
  35. data/bin/rails-dummy-app +3 -0
  36. data/bin/rails-engine +1 -11
  37. data/bin/rails-new +55 -0
  38. data/bin/rails-sandbox +1 -14
  39. data/bin/rspec +10 -0
  40. data/bin/sandbox +12 -74
  41. data/bin/setup +1 -0
  42. data/bin/update-migrations +56 -0
  43. data/codecov.yml +12 -0
  44. data/config/locales/en.yml +16 -1
  45. data/config/routes.rb +5 -11
  46. data/coverage.rb +42 -0
  47. data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
  48. data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +14 -0
  49. data/db/migrate/20230308122414_create_solidus_stripe_slug_entries.rb +12 -0
  50. data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +15 -0
  51. data/db/seeds.rb +6 -24
  52. data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
  53. data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
  54. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
  55. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
  56. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +21 -0
  57. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
  58. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
  59. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +74 -0
  60. data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +32 -0
  61. data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
  62. data/lib/solidus_stripe/configuration.rb +24 -3
  63. data/lib/solidus_stripe/engine.rb +19 -6
  64. data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
  65. data/lib/solidus_stripe/refunds_synchronizer.rb +98 -0
  66. data/lib/solidus_stripe/seeds.rb +19 -0
  67. data/lib/solidus_stripe/testing_support/factories.rb +153 -0
  68. data/lib/solidus_stripe/version.rb +1 -1
  69. data/lib/solidus_stripe/webhook/event.rb +91 -0
  70. data/lib/solidus_stripe.rb +0 -2
  71. data/solidus_stripe.gemspec +29 -6
  72. data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
  73. data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
  74. data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
  75. data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
  76. data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
  77. data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
  78. data/spec/models/solidus_stripe/customer_spec.rb +47 -0
  79. data/spec/models/solidus_stripe/gateway_spec.rb +281 -0
  80. data/spec/models/solidus_stripe/payment_intent_spec.rb +78 -0
  81. data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
  82. data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
  83. data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
  84. data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
  85. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
  86. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
  87. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
  88. data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +56 -0
  89. data/spec/solidus_stripe_spec_helper.rb +10 -0
  90. data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
  91. data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
  92. data/spec/support/solidus_stripe/backend_test_helper.rb +185 -0
  93. data/spec/support/solidus_stripe/checkout_test_helper.rb +365 -0
  94. data/spec/support/solidus_stripe/factories.rb +5 -0
  95. data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
  96. data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
  97. data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
  98. data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +145 -0
  99. data/spec/system/frontend/.keep +0 -0
  100. data/spec/system/frontend/solidus_stripe/checkout_spec.rb +206 -0
  101. data/tmp/.keep +0 -0
  102. metadata +196 -75
  103. data/.rubocop_todo.yml +0 -298
  104. data/.travis.yml +0 -28
  105. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
  106. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
  107. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
  108. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
  109. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
  110. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
  111. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
  112. data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
  113. data/app/controllers/spree/stripe_controller.rb +0 -13
  114. data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
  115. data/app/decorators/models/spree/payment_decorator.rb +0 -11
  116. data/app/decorators/models/spree/refund_decorator.rb +0 -9
  117. data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
  118. data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
  119. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
  120. data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
  121. data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
  122. data/bin/r +0 -13
  123. data/bin/sandbox_rails +0 -18
  124. data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
  125. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
  126. data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
  127. data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
  128. data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
  129. data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
  130. data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
  131. data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
  132. data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
  133. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
  134. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
  135. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
  136. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
  137. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
  138. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
  139. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
  140. data/spec/features/stripe_checkout_spec.rb +0 -486
  141. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
  142. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
  143. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
  144. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
  145. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -350
  146. data/spec/requests/payment_requests_spec.rb +0 -152
  147. data/spec/spec_helper.rb +0 -37
  148. data/spec/support/solidus_address_helper.rb +0 -15
@@ -0,0 +1,56 @@
1
+ require "solidus_stripe_spec_helper"
2
+
3
+ RSpec.describe SolidusStripe::WebhooksController, type: [:request, :webhook_request] do
4
+ describe "POST /create" do
5
+ let(:payment_method) { create(:solidus_stripe_payment_method) }
6
+ let(:stripe_payment_intent) {
7
+ payment_method.gateway.request {
8
+ Stripe::PaymentIntent.create(amount: 100, currency: 'usd')
9
+ }
10
+ }
11
+ let(:context) do
12
+ SolidusStripe::Webhook::EventWithContextFactory.from_object(
13
+ object: stripe_payment_intent,
14
+ type: "payment_intent.created",
15
+ payment_method: payment_method
16
+ )
17
+ end
18
+
19
+ context "when the request is valid" do
20
+ around do |example|
21
+ if Spree::Bus.registry.registered?(:"stripe.payment_intent.created")
22
+ example.run
23
+ else
24
+ Spree::Bus.register(:"stripe.payment_intent.created")
25
+ example.run
26
+ Spree::Bus.registry.unregister(:"stripe.payment_intent.created")
27
+ end
28
+ end
29
+
30
+ it "triggers a matching event on Spree::Bus" do
31
+ event_type = nil
32
+ subscription = Spree::Bus.subscribe(:"stripe.payment_intent.created") { |event| event_type = event.type }
33
+
34
+ webhook_request(context)
35
+
36
+ expect(event_type).to eq("payment_intent.created")
37
+ ensure
38
+ Spree::Bus.unsubscribe(subscription)
39
+ end
40
+
41
+ it "returns a 200 status code" do
42
+ webhook_request(context)
43
+
44
+ expect(response).to have_http_status(:ok)
45
+ end
46
+ end
47
+
48
+ context "when the event can't be generated" do
49
+ it "returns a 400 status code" do
50
+ webhook_request(context, timestamp: Time.zone.yesterday.to_time)
51
+
52
+ expect(response).to have_http_status(:bad_request)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stripe'
4
+ require 'solidus_starter_frontend_spec_helper'
5
+
6
+ Dir["#{__dir__}/support/solidus_stripe/**/*.rb"].sort.each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ config.include SolidusStripe::Webhook::RequestHelper, type: :webhook_request
10
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe_spec_helper"
4
+
5
+ RSpec.describe SolidusStripe::Webhook::ChargeSubscriber do
6
+ describe "#sync_refunds" do
7
+ it "synchronizes refunds" do
8
+ SolidusStripe::Seeds.refund_reasons
9
+ payment_method = create(:solidus_stripe_payment_method)
10
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
11
+ payment = create(:payment,
12
+ amount: 10,
13
+ payment_method: payment_method,
14
+ response_code: stripe_payment_intent.id,
15
+ state: "completed")
16
+ stripe_charge = Stripe::Charge.construct_from(id: "ch_123", payment_intent: "pi_123")
17
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
18
+ Stripe::ListObject.construct_from(
19
+ data: [{ id: "re_123", amount: 1000, currency: "usd", metadata: {} }]
20
+ )
21
+ )
22
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
23
+ payment_method: payment_method,
24
+ object: stripe_charge,
25
+ type: "charge.refunded"
26
+ ).solidus_stripe_object
27
+
28
+ described_class.new.sync_refunds(event)
29
+
30
+ expect(payment.refunds.count).to be(1)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe_spec_helper"
4
+
5
+ RSpec.describe SolidusStripe::Webhook::PaymentIntentSubscriber do
6
+ describe "#capture_payment" do
7
+ context "when a full capture is performed" do
8
+ it "completes a pending payment" do
9
+ payment_method = create(:solidus_stripe_payment_method)
10
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
11
+ id: "pi_123",
12
+ amount: 1000,
13
+ amount_received: 1000,
14
+ currency: "usd"
15
+ )
16
+ payment = create(:payment,
17
+ amount: 10,
18
+ payment_method: payment_method,
19
+ response_code: stripe_payment_intent.id,
20
+ state: "pending")
21
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
22
+ payment_method: payment_method,
23
+ object: stripe_payment_intent,
24
+ type: "payment_intent.succeeded"
25
+ ).solidus_stripe_object
26
+
27
+ described_class.new.capture_payment(event)
28
+
29
+ expect(payment.reload.state).to eq "completed"
30
+ end
31
+
32
+ it "adds a log entry to the payment" do
33
+ payment_method = create(:solidus_stripe_payment_method)
34
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
35
+ id: "pi_123",
36
+ amount: 1000,
37
+ amount_received: 1000,
38
+ currency: "usd"
39
+ )
40
+ payment = create(:payment,
41
+ amount: 10,
42
+ payment_method: payment_method,
43
+ response_code: stripe_payment_intent.id,
44
+ state: "pending")
45
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
46
+ payment_method: payment_method,
47
+ object: stripe_payment_intent,
48
+ type: "payment_intent.succeeded"
49
+ ).solidus_stripe_object
50
+
51
+ described_class.new.capture_payment(event)
52
+
53
+ details = payment.log_entries.last.parsed_details
54
+ expect(details.success?).to be(true)
55
+ expect(
56
+ details.message
57
+ ).to eq "Capture was successful after payment_intent.succeeded webhook"
58
+ end
59
+ end
60
+
61
+ context "when a partial capture is performed" do
62
+ it "completes a pending payment" do
63
+ SolidusStripe::Seeds.refund_reasons
64
+ payment_method = create(:solidus_stripe_payment_method)
65
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
66
+ id: "pi_123",
67
+ amount: 1000,
68
+ amount_received: 700,
69
+ currency: "usd"
70
+ )
71
+ payment = create(:payment,
72
+ amount: 10,
73
+ payment_method: payment_method,
74
+ response_code: stripe_payment_intent.id,
75
+ state: "pending")
76
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
77
+ Stripe::ListObject.construct_from(
78
+ data: [{ id: "re_123", amount: 700, currency: "usd", metadata: {} }]
79
+ )
80
+ )
81
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
82
+ payment_method: payment_method,
83
+ object: stripe_payment_intent,
84
+ type: "payment_intent.succeeded"
85
+ ).solidus_stripe_object
86
+
87
+ described_class.new.capture_payment(event)
88
+
89
+ expect(payment.reload.state).to eq "completed"
90
+ end
91
+
92
+ it "synchronizes refunds" do
93
+ SolidusStripe::Seeds.refund_reasons
94
+ payment_method = create(:solidus_stripe_payment_method)
95
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
96
+ id: "pi_123",
97
+ amount: 1000,
98
+ amount_received: 700,
99
+ currency: "usd"
100
+ )
101
+ payment = create(:payment,
102
+ amount: 7,
103
+ payment_method: payment_method,
104
+ response_code: stripe_payment_intent.id,
105
+ state: "pending")
106
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
107
+ Stripe::ListObject.construct_from(
108
+ data: [{ id: "re_123", amount: 200, currency: "usd", metadata: {} }]
109
+ )
110
+ )
111
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
112
+ payment_method: payment_method,
113
+ object: stripe_payment_intent,
114
+ type: "payment_intent.succeeded"
115
+ ).solidus_stripe_object
116
+
117
+ described_class.new.capture_payment(event)
118
+
119
+ expect(payment.refunds.count).to be(1)
120
+ end
121
+
122
+ it "adds a log entry for the captured payment" do
123
+ SolidusStripe::Seeds.refund_reasons
124
+ payment_method = create(:solidus_stripe_payment_method)
125
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
126
+ id: "pi_123",
127
+ amount: 1000,
128
+ amount_received: 700,
129
+ currency: "usd"
130
+ )
131
+ payment = create(:payment,
132
+ amount: 10,
133
+ payment_method: payment_method,
134
+ response_code: stripe_payment_intent.id,
135
+ state: "pending")
136
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
137
+ Stripe::ListObject.construct_from(
138
+ data: [{ id: "re_123", amount: 700, currency: "usd", metadata: {} }]
139
+ )
140
+ )
141
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
142
+ payment_method: payment_method,
143
+ object: stripe_payment_intent,
144
+ type: "payment_intent.succeeded"
145
+ ).solidus_stripe_object
146
+
147
+ described_class.new.capture_payment(event)
148
+
149
+ log_entries = payment.log_entries.map { [_1.parsed_details.success?, _1.parsed_details.message] }
150
+ expect(
151
+ log_entries
152
+ ).to include([true, "Capture was successful after payment_intent.succeeded webhook"])
153
+ end
154
+ end
155
+
156
+ it "does nothing if the payment is already completed" do
157
+ payment_method = create(:solidus_stripe_payment_method)
158
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
159
+ payment = create(:payment,
160
+ payment_method: payment_method,
161
+ response_code: stripe_payment_intent.id,
162
+ state: "completed")
163
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
164
+ payment_method: payment_method,
165
+ object: stripe_payment_intent,
166
+ type: "payment_intent.succeeded",
167
+ ).solidus_stripe_object
168
+
169
+ described_class.new.capture_payment(event)
170
+
171
+ expect(payment.reload.state).to eq "completed"
172
+ expect(payment.log_entries.count).to be(0)
173
+ end
174
+ end
175
+
176
+ describe "#fail_payment" do
177
+ it "fails a pending payment" do
178
+ payment_method = create(:solidus_stripe_payment_method)
179
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
180
+ payment = create(:payment,
181
+ payment_method: payment_method,
182
+ response_code: stripe_payment_intent.id,
183
+ state: "pending")
184
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
185
+ payment_method: payment_method,
186
+ object: stripe_payment_intent,
187
+ type: "payment_intent.payment_failed"
188
+ ).solidus_stripe_object
189
+
190
+ described_class.new.fail_payment(event)
191
+
192
+ expect(payment.reload.state).to eq "failed"
193
+ end
194
+
195
+ it "adds a log entry to the payment" do
196
+ payment_method = create(:solidus_stripe_payment_method)
197
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
198
+ payment = create(:payment,
199
+ payment_method: payment_method,
200
+ response_code: stripe_payment_intent.id,
201
+ state: "pending")
202
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
203
+ payment_method: payment_method,
204
+ object: stripe_payment_intent,
205
+ type: "payment_intent.payment_failed"
206
+ ).solidus_stripe_object
207
+
208
+ described_class.new.fail_payment(event)
209
+
210
+ details = payment.log_entries.last.parsed_details
211
+ expect(details.success?).to be(false)
212
+ expect(
213
+ details.message
214
+ ).to eq "Payment was marked as failed after payment_intent.failed webhook"
215
+ end
216
+
217
+ it "does nothing if the payment is already failed" do
218
+ payment_method = create(:solidus_stripe_payment_method)
219
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
220
+ payment = create(:payment,
221
+ payment_method: payment_method,
222
+ response_code: stripe_payment_intent.id,
223
+ state: "failed")
224
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
225
+ payment_method: payment_method,
226
+ object: stripe_payment_intent,
227
+ type: "payment_intent.payment_failed",
228
+ ).solidus_stripe_object
229
+
230
+ described_class.new.fail_payment(event)
231
+
232
+ expect(payment.reload.state).to eq "failed"
233
+ expect(payment.log_entries.count).to be(0)
234
+ end
235
+ end
236
+
237
+ describe "#void_payment" do
238
+ it "voids a pending payment" do
239
+ payment_method = create(:solidus_stripe_payment_method)
240
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
241
+ payment = create(:payment,
242
+ payment_method: payment_method,
243
+ response_code: stripe_payment_intent.id,
244
+ state: "pending")
245
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
246
+ payment_method: payment_method,
247
+ object: stripe_payment_intent,
248
+ type: "payment_intent.canceled"
249
+ ).solidus_stripe_object
250
+
251
+ described_class.new.void_payment(event)
252
+
253
+ expect(payment.reload.state).to eq "void"
254
+ end
255
+
256
+ it "adds a log entry to the payment" do
257
+ payment_method = create(:solidus_stripe_payment_method)
258
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
259
+ payment = create(:payment,
260
+ payment_method: payment_method,
261
+ response_code: stripe_payment_intent.id,
262
+ state: "pending")
263
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
264
+ payment_method: payment_method,
265
+ object: stripe_payment_intent,
266
+ type: "payment_intent.canceled"
267
+ ).solidus_stripe_object
268
+
269
+ described_class.new.void_payment(event)
270
+
271
+ details = payment.log_entries.last.parsed_details
272
+ expect(details.success?).to be(true)
273
+ expect(
274
+ details.message
275
+ ).to eq "Payment was voided after payment_intent.voided webhook (duplicate)"
276
+ end
277
+
278
+ it "does nothing if the payment is already voided" do
279
+ payment_method = create(:solidus_stripe_payment_method)
280
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
281
+ payment = create(:payment,
282
+ payment_method: payment_method,
283
+ response_code: stripe_payment_intent.id,
284
+ state: "void")
285
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
286
+ payment_method: payment_method,
287
+ object: stripe_payment_intent,
288
+ type: "payment_intent.payment_void",
289
+ ).solidus_stripe_object
290
+
291
+ described_class.new.void_payment(event)
292
+
293
+ expect(payment.reload.state).to eq "void"
294
+ expect(payment.log_entries.count).to be(0)
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe::BackendTestHelper
4
+ def create_payment_method(setup_future_usage: 'off_session', auto_capture: false)
5
+ @payment_method = create(
6
+ :solidus_stripe_payment_method,
7
+ preferred_setup_future_usage: setup_future_usage,
8
+ auto_capture: auto_capture
9
+ )
10
+ end
11
+
12
+ def payment_method
13
+ # Memoize the payment method id to avoid fetching it multiple times
14
+ @payment_method ||= SolidusStripe::PaymentMethod.first || create_payment_method
15
+ end
16
+
17
+ def order
18
+ @order ||= create(:completed_order_with_totals, line_items_price: 50)
19
+ end
20
+
21
+ def last_valid_payment
22
+ order.payments.valid.last
23
+ end
24
+
25
+ # Stripe-related helper methods for creating and fetching Stripe objects
26
+
27
+ def create_stripe_payment_method(card_number)
28
+ payment_method.gateway.request do
29
+ Stripe::PaymentMethod.create({
30
+ type: 'card',
31
+ card: {
32
+ number: card_number,
33
+ exp_month: 12,
34
+ exp_year: (Time.zone.now.year + 1),
35
+ cvc: '123',
36
+ },
37
+ })
38
+ end
39
+ end
40
+
41
+ def create_stripe_payment_intent(stripe_payment_method_id)
42
+ payment_method.gateway.request do
43
+ Stripe::PaymentIntent.create(
44
+ amount: (order.outstanding_balance * 100).to_i,
45
+ capture_method: 'manual',
46
+ confirm: true,
47
+ currency: 'usd',
48
+ payment_method: stripe_payment_method_id
49
+ )
50
+ end
51
+ end
52
+
53
+ def fetch_stripe_refund(payment)
54
+ payment_method.gateway.request do
55
+ Stripe::Refund.list(payment_intent: payment.response_code).data.first
56
+ end
57
+ end
58
+
59
+ def fetch_stripe_payment_intent(payment)
60
+ payment_method.gateway.request do
61
+ Stripe::PaymentIntent.retrieve(payment.response_code)
62
+ end
63
+ end
64
+
65
+ # Navigation helper methods for interacting with the admin panel
66
+
67
+ def visit_payments_page
68
+ visit "/admin/orders/#{order.number}/payments"
69
+ end
70
+
71
+ def visit_payment_page(payment)
72
+ visit_payments_page
73
+ click_on payment.number
74
+ end
75
+
76
+ # Action helper methods for performing operations on payments
77
+
78
+ def create_authorized_payment(opts = {})
79
+ stripe_payment_method = create_stripe_payment_method(opts[:card_number] || '4242424242424242')
80
+ stripe_payment_intent = create_stripe_payment_intent(stripe_payment_method.id)
81
+
82
+ create(:solidus_stripe_payment,
83
+ :authorized,
84
+ order: order,
85
+ response_code: stripe_payment_intent.id,
86
+ stripe_payment_method_id: stripe_payment_method.id,
87
+ payment_method: payment_method)
88
+ end
89
+
90
+ def create_captured_payment(card_number: '4242424242424242')
91
+ payment = create_authorized_payment(card_number: card_number)
92
+ payment.capture!
93
+ payment
94
+ end
95
+
96
+ def create_valid_payment_source
97
+ stripe_payment_method = create_stripe_payment_method('4242424242424242')
98
+ SolidusStripe::PaymentSource.create!(
99
+ stripe_payment_method_id: stripe_payment_method.id,
100
+ payment_method_id: payment_method.id
101
+ )
102
+ end
103
+
104
+ def create_order_with_existing_payment_source
105
+ payment_source = create_valid_payment_source
106
+ @order = create(:order_with_line_items, line_items_price: 50)
107
+ order.user.wallet.add(payment_source)
108
+ end
109
+
110
+ def complete_order_with_existing_payment_source
111
+ visit_payments_page
112
+ click_on "Continue"
113
+ click_on "Confirm"
114
+ click_on "Complete Order"
115
+ end
116
+
117
+ def refund_payment
118
+ refund_reason = create :refund_reason
119
+ click_icon(:"mail-reply") # Refund icon style reference in solidus_backend
120
+ within '.new_refund' do
121
+ select refund_reason.name, from: 'Reason'
122
+ click_button 'Refund'
123
+ end
124
+ end
125
+
126
+ def partially_refund_payment(refund_reason, amount)
127
+ click_icon(:"mail-reply") # Refund icon style reference in solidus_backend
128
+ within '.new_refund' do
129
+ select refund_reason.name, from: 'Reason'
130
+ fill_in 'Amount', with: amount
131
+ click_button 'Refund'
132
+ end
133
+ end
134
+
135
+ def capture_partial_payment_amount(payment, amount)
136
+ within_row(1) do
137
+ click_icon(:edit)
138
+ fill_in('amount', with: amount)
139
+ click_icon(:ok)
140
+ expect(page).to have_selector('td.amount span', text: amount.to_money.format)
141
+ expect(payment.reload.amount).to eq(amount)
142
+ end
143
+
144
+ capture_payment
145
+ end
146
+
147
+ def void_payment
148
+ click_icon(:void)
149
+ end
150
+
151
+ def capture_payment
152
+ click_icon(:capture)
153
+ end
154
+
155
+ def cancel_order
156
+ visit "/admin/orders/#{order.number}/edit"
157
+ accept_alert do
158
+ click_on 'Cancel'
159
+ end
160
+ end
161
+
162
+ # Helper methods for checking expected outcomes and states
163
+
164
+ def expects_page_to_display_log_messages(log_messages = [])
165
+ expect(page).to have_content('Log Entries')
166
+ log_messages.each do |log_message|
167
+ expect(page).to have_content(log_message)
168
+ end
169
+ end
170
+
171
+ def expects_payment_to_be_refunded_on_stripe(payment, amount)
172
+ stripe_refund = fetch_stripe_refund(payment)
173
+ expect(stripe_refund.amount).to eq(amount * 100)
174
+ end
175
+
176
+ def expects_payment_to_be_voided_on_stripe(payment)
177
+ stripe_payment_intent = fetch_stripe_payment_intent(payment)
178
+ expect(stripe_payment_intent.status).to eq('canceled')
179
+ end
180
+
181
+ def expects_payment_to_have_correct_capture_amount_on_stripe(payment, amount)
182
+ stripe_payment_intent = fetch_stripe_payment_intent(payment)
183
+ expect(stripe_payment_intent.amount_received).to eq(amount.to_money.fractional)
184
+ end
185
+ end