solidus_stripe 4.4.1 → 5.0.0.alpha.1

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +92 -52
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +94 -4
  5. data/.yardopts +1 -0
  6. data/Gemfile +10 -30
  7. data/LICENSE +2 -2
  8. data/Procfile.dev +3 -0
  9. data/README.md +145 -215
  10. data/Rakefile +5 -44
  11. data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
  12. data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
  13. data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
  14. data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
  15. data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
  16. data/app/models/solidus_stripe/customer.rb +21 -0
  17. data/app/models/solidus_stripe/gateway.rb +231 -0
  18. data/app/models/solidus_stripe/payment_intent.rb +111 -0
  19. data/app/models/solidus_stripe/payment_method.rb +106 -0
  20. data/app/models/solidus_stripe/payment_source.rb +31 -0
  21. data/app/models/solidus_stripe/slug_entry.rb +20 -0
  22. data/app/models/solidus_stripe.rb +7 -0
  23. data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
  24. data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
  25. data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
  26. data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +14 -0
  27. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
  28. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
  29. data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
  30. data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
  31. data/bin/dev +13 -0
  32. data/bin/dummy-app +29 -0
  33. data/bin/rails +38 -3
  34. data/bin/rails-dummy-app +3 -0
  35. data/bin/rails-engine +1 -11
  36. data/bin/rails-new +55 -0
  37. data/bin/rails-sandbox +1 -14
  38. data/bin/rspec +10 -0
  39. data/bin/sandbox +12 -74
  40. data/bin/setup +1 -0
  41. data/bin/update-migrations +56 -0
  42. data/codecov.yml +12 -0
  43. data/config/locales/en.yml +16 -1
  44. data/config/routes.rb +5 -11
  45. data/coverage.rb +42 -0
  46. data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
  47. data/db/migrate/20230303154931_create_solidus_stripe_setup_intent.rb +10 -0
  48. data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +10 -0
  49. data/db/migrate/20230308122414_create_solidus_stripe_webhook_endpoints.rb +10 -0
  50. data/db/migrate/20230310152615_add_payment_method_reference_to_stripe_intents.rb +6 -0
  51. data/db/migrate/20230310171444_normalize_stripe_intent_id_attributes.rb +6 -0
  52. data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +16 -0
  53. data/db/migrate/20230323154931_drop_solidus_stripe_setup_intent.rb +13 -0
  54. data/db/migrate/20230403094916_rename_webhook_endpoint_to_payment_method_slug_entries.rb +5 -0
  55. data/db/seeds.rb +6 -24
  56. data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
  57. data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
  58. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
  59. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
  60. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +16 -0
  61. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
  62. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
  63. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +39 -0
  64. data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +20 -0
  65. data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
  66. data/lib/solidus_stripe/configuration.rb +24 -3
  67. data/lib/solidus_stripe/engine.rb +19 -6
  68. data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
  69. data/lib/solidus_stripe/refunds_synchronizer.rb +96 -0
  70. data/lib/solidus_stripe/seeds.rb +19 -0
  71. data/lib/solidus_stripe/testing_support/factories.rb +153 -0
  72. data/lib/solidus_stripe/version.rb +1 -1
  73. data/lib/solidus_stripe/webhook/event.rb +90 -0
  74. data/lib/solidus_stripe.rb +0 -2
  75. data/solidus_stripe.gemspec +29 -6
  76. data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
  77. data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
  78. data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
  79. data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
  80. data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
  81. data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
  82. data/spec/models/solidus_stripe/customer_spec.rb +47 -0
  83. data/spec/models/solidus_stripe/gateway_spec.rb +283 -0
  84. data/spec/models/solidus_stripe/payment_intent_spec.rb +17 -0
  85. data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
  86. data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
  87. data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
  88. data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
  89. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
  90. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
  91. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
  92. data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +52 -0
  93. data/spec/solidus_stripe_spec_helper.rb +10 -0
  94. data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
  95. data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
  96. data/spec/support/solidus_stripe/backend_test_helper.rb +210 -0
  97. data/spec/support/solidus_stripe/checkout_test_helper.rb +339 -0
  98. data/spec/support/solidus_stripe/factories.rb +5 -0
  99. data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
  100. data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
  101. data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
  102. data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +119 -0
  103. data/spec/system/frontend/.keep +0 -0
  104. data/spec/system/frontend/solidus_stripe/checkout_spec.rb +187 -0
  105. data/tmp/.keep +0 -0
  106. metadata +202 -78
  107. data/.rubocop_todo.yml +0 -298
  108. data/.travis.yml +0 -28
  109. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
  110. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
  111. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
  112. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
  113. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
  114. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
  115. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
  116. data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
  117. data/app/controllers/spree/stripe_controller.rb +0 -13
  118. data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
  119. data/app/decorators/models/spree/payment_decorator.rb +0 -11
  120. data/app/decorators/models/spree/refund_decorator.rb +0 -9
  121. data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
  122. data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
  123. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
  124. data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
  125. data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
  126. data/bin/r +0 -13
  127. data/bin/sandbox_rails +0 -18
  128. data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
  129. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
  130. data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
  131. data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
  132. data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
  133. data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
  134. data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
  135. data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
  136. data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
  137. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
  138. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
  139. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
  140. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
  141. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
  142. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
  143. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
  144. data/spec/features/stripe_checkout_spec.rb +0 -486
  145. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
  146. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
  147. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
  148. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
  149. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -354
  150. data/spec/requests/payment_requests_spec.rb +0 -152
  151. data/spec/solidus_frontend_app_template.rb +0 -17
  152. data/spec/spec_helper.rb +0 -37
  153. data/spec/support/solidus_address_helper.rb +0 -15
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_starter_frontend_spec_helper'
4
+
5
+ module SolidusStripe::CheckoutTestHelper
6
+ include SolidusStarterFrontend::SystemHelpers
7
+ def self.included(base)
8
+ base.include Devise::Test::IntegrationHelpers
9
+ end
10
+
11
+ # Setup methods
12
+ #
13
+ # These are methods that are used specifically for setting up the
14
+ # environment for testing.
15
+
16
+ def assign_guest_token(guest_token)
17
+ # rubocop:disable RSpec/AnyInstance
18
+ allow_any_instance_of(ActionDispatch::Cookies::SignedKeyRotatingCookieJar).tap do |allow_cookie_jar|
19
+ # Retrieve all other cookies from the original jar.
20
+ allow_cookie_jar.to receive(:[]).and_call_original
21
+ allow_cookie_jar.to receive(:[]).with(:guest_token).and_return(guest_token)
22
+ end
23
+ # rubocop:enable RSpec/AnyInstance
24
+ end
25
+
26
+ def create_payment_method(setup_future_usage: 'off_session', auto_capture: false)
27
+ @payment_method = create(
28
+ :stripe_payment_method,
29
+ preferred_setup_future_usage: setup_future_usage,
30
+ auto_capture: auto_capture
31
+ )
32
+ end
33
+
34
+ def payment_method
35
+ # Memoize the payment method id to avoid fetching it multiple times
36
+ @payment_method ||= SolidusStripe::PaymentMethod.first!
37
+ end
38
+
39
+ def current_order
40
+ @current_order ||= Spree::Order.last!
41
+ end
42
+
43
+ def last_stripe_payment
44
+ current_order.payments.reorder(id: :desc).find_by(source_type: "SolidusStripe::PaymentSource")
45
+ end
46
+
47
+ def capture_last_valid_payment
48
+ payment = current_order.payments.valid.last
49
+ payment.capture!
50
+ expect(payment.payment_method.type).to eq('SolidusStripe::PaymentMethod')
51
+ intent = payment.payment_method.gateway.request do
52
+ Stripe::PaymentIntent.retrieve(payment.response_code)
53
+ end
54
+ expect(intent.status).to eq('succeeded')
55
+ end
56
+
57
+ # Stripe form methods
58
+ #
59
+ # These are methods that are used specifically for interacting with
60
+ # the Stripe payment form.
61
+
62
+ def fill_stripe_form(
63
+ number: 4242_4242_4242_4242, # rubocop:disable Style/NumericLiterals
64
+ expiry_month: 12,
65
+ expiry_year: Time.current.year + 1,
66
+ date: nil,
67
+ cvc: '123',
68
+ country: 'United States',
69
+ zip: '90210'
70
+ )
71
+ fill_in_stripe_cvc(cvc)
72
+ fill_in_stripe_expiry_date(year: expiry_year, month: expiry_month, date: date)
73
+ fill_in_stripe_card(number)
74
+ fill_in_stripe_country(country)
75
+ fill_in_stripe_zip(zip) if zip # not shown for every country
76
+ end
77
+
78
+ def fill_in_stripe_card(number)
79
+ fills_in_stripe_input 'number', with: number
80
+ end
81
+
82
+ def fill_in_stripe_expiry_date(year: nil, month: nil, date: nil)
83
+ date ||= begin
84
+ month = month.to_s.rjust(2, '0') unless month.is_a? String
85
+ year = year.to_s[2..3] unless year.is_a? String
86
+ "#{month}#{year}"
87
+ end
88
+
89
+ fills_in_stripe_input 'expiry', with: date.to_s[0..3]
90
+ end
91
+
92
+ def fill_in_stripe_cvc(cvc)
93
+ fills_in_stripe_input 'cvc', with: cvc.to_s[0..2].to_s
94
+ end
95
+
96
+ def fill_in_stripe_country(country_name)
97
+ using_wait_time(10) do
98
+ within_frame(find_stripe_iframe) do
99
+ find(%{select[name="country"]}).select(country_name)
100
+ end
101
+ end
102
+ end
103
+
104
+ def fill_in_stripe_zip(zip)
105
+ fills_in_stripe_input 'postalCode', with: zip
106
+ end
107
+
108
+ def fills_in_stripe_input(name, with:)
109
+ using_wait_time(10) do
110
+ within_frame(find_stripe_iframe) do
111
+ with.to_s.chars.each { find(%{input[name="#{name}"]}).send_keys(_1) }
112
+ end
113
+ end
114
+ end
115
+
116
+ def clear_stripe_form
117
+ %w[number expiry cvc postalCode].each do |name|
118
+ using_wait_time(10) do
119
+ within_frame(find_stripe_iframe) do
120
+ field = find(%{input[name="#{name}"]})
121
+ field.value.length.times { field.send_keys [:backspace] }
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def find_stripe_iframe
128
+ fieldset = find_payment_fieldset(payment_method.id)
129
+ expect(fieldset).to have_css('iframe', wait: 15) # trigger waiting if the frame is not yet there
130
+ fieldset.find("iframe")
131
+ end
132
+
133
+ # 3D Secure methods
134
+ #
135
+ # These are methods that are used specifically for handling 3D Secure (3DS) payment
136
+ # authorizations.
137
+ #
138
+ # However, it's important to note that this process may require an additional step,
139
+ # (currently not fully supported), which is indicated by the "next_action" property
140
+ # of the Stripe PaymentIntent object.
141
+ #
142
+ # More information on this property can be found in the Stripe API documentation:
143
+ # PaymentIntent objects : https://stripe.com/docs/api/payment_intents/object#payment_intent_object-next_action
144
+
145
+ def authorize_3d_secure_payment(authenticate: true)
146
+ find_frame('body > div > iframe') do
147
+ find_frame('#challengeFrame') do
148
+ find_frame("iframe[name='acsFrame']") do
149
+ click_on authenticate ? 'Complete authentication' : 'Fail authentication'
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def authorize_3d_secure_2_payment(authenticate: true)
156
+ find_frame('body > div > iframe') do
157
+ find_frame('#challengeFrame') do
158
+ click_on authenticate ? 'Complete' : 'Fail'
159
+ end
160
+ end
161
+ end
162
+
163
+ def find_frame(selector, &block)
164
+ using_wait_time(15) do
165
+ frame = find(selector)
166
+ within_frame(frame, &block)
167
+ end
168
+ end
169
+
170
+ # Checkout methods
171
+ #
172
+ # These are methods that are used specifically for interacting with
173
+ # the checkout process.
174
+
175
+ def visit_payment_step(user: nil)
176
+ @current_order = Spree::TestingSupport::OrderWalkthrough.up_to(:delivery, user: user)
177
+
178
+ if user
179
+ sign_in current_order.user
180
+ else
181
+ assign_guest_token current_order.guest_token
182
+ end
183
+
184
+ visit '/checkout/payment'
185
+ end
186
+
187
+ def choose_new_stripe_payment
188
+ choose(option: payment_method.id)
189
+ end
190
+
191
+ def submit_payment
192
+ click_button("Save and Continue")
193
+ end
194
+
195
+ def check_terms_of_service
196
+ expect(page).to have_content("Agree to Terms of Service")
197
+ check "Agree to Terms of Service"
198
+ end
199
+
200
+ def confirm_order
201
+ click_button("Place Order")
202
+ end
203
+
204
+ def complete_order
205
+ check_terms_of_service
206
+ confirm_order
207
+ expect(page).to have_content('Your order has been processed successfully')
208
+ end
209
+
210
+ def expects_page_and_order_to_be_in_payment_step
211
+ expect(page).to have_current_path('/checkout/payment')
212
+ expect(current_order.state).to eq('payment')
213
+ end
214
+
215
+ def expects_payment_to_be_failed
216
+ expect(last_stripe_payment.state).to eq('failed')
217
+ end
218
+
219
+ def expects_page_to_not_display_wallet_payment_sources
220
+ expect(page).to have_no_selector("[name='order[wallet_payment_source_id]']")
221
+ end
222
+
223
+ def expects_to_have_specific_authorized_amount_on_stripe(amount)
224
+ stripe_payment_intent = payment_method.gateway.request do
225
+ Stripe::PaymentIntent.retrieve(last_stripe_payment.response_code)
226
+ end
227
+ expect(stripe_payment_intent.amount).to eq(amount * 100)
228
+ end
229
+
230
+ # Test methods
231
+ #
232
+ # These are methods that are used specifically for testing the Stripe
233
+ # checkout process.
234
+
235
+ def payment_intent_is_created_with_required_capture
236
+ intent = SolidusStripe::PaymentIntent.where(
237
+ payment_method: payment_method,
238
+ order: current_order
239
+ ).last.stripe_intent
240
+ expect(intent.status).to eq('requires_capture')
241
+ end
242
+
243
+ def payment_intent_is_created_and_successfully_captured
244
+ order = Spree::Order.last
245
+ intent = SolidusStripe::PaymentIntent.where(
246
+ payment_method: payment_method,
247
+ order: order
248
+ ).last.stripe_intent
249
+ expect(intent.status).to eq('succeeded')
250
+ end
251
+
252
+ def payment_intent_is_created_with_required_action
253
+ intent = SolidusStripe::PaymentIntent.where(
254
+ payment_method: payment_method,
255
+ order: current_order
256
+ ).last.stripe_intent
257
+ expect(intent.status).to eq('requires_action')
258
+ end
259
+
260
+ def invalid_data_are_notified
261
+ fill_in_stripe_country('United States')
262
+
263
+ [
264
+ [{ number: '4242424242424241' }, 'Your card number is invalid'], # incorrect_number
265
+ [{ date: '1110' }, "Your card's expiration year is in the past"], # invalid_expiry_year
266
+ [{ cvc: 99 }, "Your card's security code is incomplete"] # invalid_cvc
267
+ ].each do |args, text|
268
+ clear_stripe_form
269
+ fill_stripe_form(**args)
270
+ submit_payment
271
+ using_wait_time(10) do
272
+ within_frame(find_stripe_iframe) do
273
+ expect(page).to have_content(text)
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ def incomplete_cards_are_notified
280
+ # In order to have a complete Stripe form,
281
+ # it's essential to have a Postal Code field that will only be displayed
282
+ # when the user selects United States as their country.
283
+ fill_in_stripe_country('United States')
284
+
285
+ clear_stripe_form
286
+ submit_payment
287
+ [
288
+ "Your card number is incomplete",
289
+ "Your card's expiration date is incomplete",
290
+ "Your card's security code is incomplete",
291
+ "Your postal code is incomplete"
292
+ ].each do |text|
293
+ within_frame(find_stripe_iframe) do
294
+ expect(page).to have_content(text)
295
+ end
296
+ end
297
+ end
298
+
299
+ def declined_cards_at_intent_creation_are_notified
300
+ [
301
+ # Decline codes
302
+ # https://stripe.com/docs/declines/codes
303
+ ['4000000000000002', 'Your card has been declined.'], # Generic decline
304
+ ['4000000000009995', 'Your card has insufficient funds.'], # Insufficient funds decline
305
+ ['4000000000009987', 'Your card has been declined.'], # Lost card decline
306
+ ['4000000000009979', 'Your card has been declined.'], # Stolen card decline
307
+ ['4000000000000069', 'Your card has expired.'], # Expired card decline
308
+ ['4000000000000127', "Your card's security code is incorrect."], # Incorrect CVC decline
309
+ ['4000000000000119', 'An error occurred while processing your card.'], # Processing error decline
310
+
311
+ # Fraudulent cards
312
+ # https://stripe.com/docs/testing#fraud-prevention
313
+ ['4100000000000019', 'Your card has been declined.'], # Always blocked
314
+ ].each do |number, text|
315
+ fill_in_stripe_country('United States')
316
+ fill_stripe_form(number: number)
317
+ submit_payment
318
+ check_terms_of_service
319
+ confirm_order
320
+ expect(page).to have_content(text, wait: 15)
321
+ end
322
+ end
323
+
324
+ def successfully_creates_a_payment_intent(user: nil, auto_capture: false)
325
+ visit_payment_step(user: user)
326
+ choose_new_stripe_payment
327
+ fill_stripe_form
328
+
329
+ submit_payment
330
+
331
+ complete_order
332
+
333
+ if auto_capture
334
+ payment_intent_is_created_and_successfully_captured
335
+ else
336
+ payment_intent_is_created_with_required_capture
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.definition_file_paths << SolidusStripe::Engine.root.join(
2
+ 'lib/solidus_stripe/testing_support/factories'
3
+ ).to_s
4
+
5
+ FactoryBot.reload
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ module Webhook
5
+ # Fixtures for Stripe webhook events represented as a Ruby hash.
6
+ #
7
+ # Consume with `SolidusStripe::Webhook::EventWithContextFactory.from_data`.
8
+ module DataFixtures
9
+ def self.charge_succeeded(with_webhook: true)
10
+ {
11
+ "id" => "evt_3MRUo1JvEPu9yc7w091rP2XV",
12
+ "type" => "charge.succeeded",
13
+ "object" => "event",
14
+ "api_version" => "2022-11-15",
15
+ "created" => 1_674_022_050,
16
+ "data" => {
17
+ "object" => {
18
+ "id" => "ch_3MRUo1JvEPu9yc7w0BglRG7L",
19
+ "object" => "charge",
20
+ "amount" => 2000,
21
+ "amount_captured" => 2000,
22
+ "amount_refunded" => 0,
23
+ "application" => nil,
24
+ "application_fee" => nil,
25
+ "application_fee_amount" => nil,
26
+ "balance_transaction" => "txn_3MRUo1JvEPu9yc7w0aaUukiY",
27
+ "billing_details" => {
28
+ "address" => { "city" => nil, "country" => nil, "line1" => nil, "line2" => nil, "postal_code" => nil,
29
+ "state" => nil },
30
+ "email" => nil,
31
+ "name" => nil,
32
+ "phone" => nil
33
+ },
34
+ "calculated_statement_descriptor" => "NEBULAB SRL",
35
+ "captured" => true,
36
+ "created" => 1_674_022_049,
37
+ "currency" => "usd",
38
+ "customer" => nil,
39
+ "description" => "(created by Stripe CLI)",
40
+ "destination" => nil,
41
+ "dispute" => nil,
42
+ "disputed" => false,
43
+ "failure_balance_transaction" => nil,
44
+ "failure_code" => nil,
45
+ "failure_message" => nil,
46
+ "fraud_details" => {},
47
+ "invoice" => nil,
48
+ "livemode" => false,
49
+ "metadata" => {},
50
+ "on_behalf_of" => nil,
51
+ "order" => nil,
52
+ "outcome" => { "network_status" => "approved_by_network", "reason" => nil, "risk_level" => "normal",
53
+ "risk_score" => 1, "seller_message" => "Payment complete.", "type" => "authorized" },
54
+ "paid" => true,
55
+ "payment_intent" => "pi_3MRUo1JvEPu9yc7w0ldPcSpn",
56
+ "payment_method" => "pm_1MRUo0JvEPu9yc7wVFMdYurf",
57
+ "payment_method_details" => {
58
+ "card" => { "brand" => "visa",
59
+ "checks" => { "address_line1_check" => nil, "address_postal_code_check" => nil,
60
+ "cvc_check" => nil },
61
+ "country" => "US",
62
+ "exp_month" => 1,
63
+ "exp_year" => 2024,
64
+ "fingerprint" => "pUfqdtmzdaOnI2SE",
65
+ "funding" => "credit",
66
+ "installments" => nil,
67
+ "last4" => "4242",
68
+ "mandate" => nil,
69
+ "network" => "visa",
70
+ "three_d_secure" => nil,
71
+ "wallet" => nil },
72
+ "type" => "card"
73
+ },
74
+ "receipt_email" => nil,
75
+ "receipt_number" => nil,
76
+ "receipt_url" => "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN21MdGJKdkVQdTl5Yzd3KKKZnp4GMgahzL5hXx86LBYPolrU9mdbq37sokiWbp-wT-NGJrXXxmipTFzS_AG1Wp1Rg6HWN1F2-9ek",
77
+ "refunded" => false,
78
+ "refunds" => { "object" => "list", "data" => [], "has_more" => false, "total_count" => 0,
79
+ "url" => "/v1/charges/ch_3MRUo1JvEPu9yc7w0BglRG7L/refunds" },
80
+ "review" => nil,
81
+ "shipping" => {
82
+ "address" => { "city" => "San Francisco", "country" => "US",
83
+ "line1" => "510 Townsend St", "line2" => nil,
84
+ "postal_code" => "94103", "state" => "CA" },
85
+ "carrier" => nil, "name" => "Jenny Rosen", "phone" => nil,
86
+ "tracking_number" => nil
87
+ },
88
+ "source" => nil,
89
+ "source_transfer" => nil,
90
+ "statement_descriptor" => nil,
91
+ "statement_descriptor_suffix" => nil,
92
+ "status" => "succeeded",
93
+ "transfer_data" => nil,
94
+ "transfer_group" => nil
95
+ }
96
+ },
97
+ "livemode" => false,
98
+ "pending_webhooks" => 3,
99
+ "request" => "req_CAHWxuQdLQukn0"
100
+ }.tap do |data|
101
+ data["webhook"] = charge_succeeded(with_webhook: false) if with_webhook
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe/webhook/event"
4
+ require "stripe"
5
+
6
+ module SolidusStripe
7
+ module Webhook
8
+ # Factory to create a webhook event along with its context.
9
+ #
10
+ # It allows to create Stripe webhook from different sources (hash, stripe
11
+ # object) in different representations (json, stripe event object, solidus
12
+ # stripe object, header).
13
+ #
14
+ # The context for a event is composed by the timestamp and its secret, which
15
+ # in turn affect the header representation.
16
+ class EventWithContextFactory
17
+ def self.from_data(data:, payment_method:, timestamp: Time.zone.now)
18
+ new(data: data, timestamp: timestamp, payment_method: payment_method)
19
+ end
20
+
21
+ def self.from_object(object:, type:, payment_method:, timestamp: Time.zone.now)
22
+ data_base = data_base(object, type)
23
+ data = data_base.merge("webhook" => data_base)
24
+ new(data: data, timestamp: timestamp, payment_method: payment_method)
25
+ end
26
+
27
+ def self.data_base(object, type)
28
+ {
29
+ "id" => "evt_3MRUo1JvEPu9yc7w091rP2XV",
30
+ "object" => "event",
31
+ "api_version" => "2022-11-15",
32
+ "created" => 1_674_022_050,
33
+ "data" => {
34
+ "object" => object.as_json
35
+ },
36
+ "livemode" => false,
37
+ "pending_webhooks" => 0,
38
+ "request" => {
39
+ "id" => "req_3MRUo1JvEPu9yc7w0",
40
+ "idempotency_key" => "idempotency_key"
41
+ },
42
+ "type" => type
43
+ }
44
+ end
45
+ private_class_method :data_base
46
+
47
+ attr_reader :data, :timestamp, :secret, :payment_method
48
+
49
+ def initialize(data:, payment_method:, timestamp: Time.zone.now)
50
+ @data = data
51
+ @timestamp = timestamp
52
+ @payment_method = payment_method
53
+ @secret = payment_method.preferred_webhook_endpoint_signing_secret
54
+ end
55
+
56
+ def stripe_object
57
+ @stripe_object ||= Stripe::Event.construct_from(data)
58
+ end
59
+
60
+ def solidus_stripe_object
61
+ @solidus_stripe_object = SolidusStripe::Webhook::Event.new(stripe_event: stripe_object,
62
+ spree_payment_method: @payment_method)
63
+ end
64
+
65
+ def json
66
+ @json ||= JSON.generate(data)
67
+ end
68
+
69
+ def signature_header(timestamp: self.timestamp)
70
+ @signature_header ||= Stripe::Webhook::Signature.generate_header(timestamp, signature)
71
+ end
72
+
73
+ def signature
74
+ @signature ||= Stripe::Webhook::Signature.compute_signature(timestamp, json, secret)
75
+ end
76
+
77
+ def slug
78
+ @payment_method.slug
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ module Webhook
5
+ # Helpers to simulate incoming Stripe webhook from request specs.
6
+ #
7
+ # It's a lightweight alternative to configuring `stripe-cli` in the automated tests.
8
+ module RequestHelper
9
+ # @param context [SolidusStripe::Webhook::EventWithContextFactory]
10
+ # @param payment_method [SolidusStripe::PaymentMethod]
11
+ # @param timestamp [Time] It allows to override the timestamp in the context
12
+ # to simulate an invalid request.
13
+ # @param slug [String] It allows to override the slug in the payment
14
+ # method to simulate an invalid request.
15
+ def webhook_request(context, timestamp: context.timestamp)
16
+ post "/solidus_stripe/#{context.slug}/webhooks",
17
+ params: context.json,
18
+ headers: { webhook_signature_header_key => webhook_signature_header(context, timestamp: timestamp) }
19
+ end
20
+
21
+ private
22
+
23
+ def webhook_signature_header_key
24
+ SolidusStripe::WebhooksController::SIGNATURE_HEADER
25
+ end
26
+
27
+ def webhook_signature_header(context, timestamp:)
28
+ context.signature_header(timestamp: timestamp)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_stripe_spec_helper'
4
+
5
+ RSpec.describe 'SolidusStripe Orders Payments', :js do
6
+ include SolidusStripe::BackendTestHelper
7
+ stub_authorization!
8
+
9
+ context 'with successful payment operations' do
10
+ it 'navigates to the payments page' do
11
+ payment = create_authorized_payment
12
+ visit_payments_page
13
+
14
+ expects_page_to_display_payment(payment)
15
+ end
16
+
17
+ it 'displays log entries for a payment' do
18
+ payment = create(:stripe_payment, :captured, order: order, payment_method: payment_method)
19
+
20
+ visit_payment_page(payment)
21
+
22
+ expects_page_to_display_log_messages(["PaymentIntent was confirmed and captured successfully"])
23
+ end
24
+
25
+ it 'captures an authorized payment successfully' do
26
+ payment = create_authorized_payment
27
+ visit_payments_page
28
+
29
+ capture_payment
30
+
31
+ expects_page_to_display_successfully_captured_payment(payment)
32
+ expects_payment_to_have_correct_capture_amount_on_stripe(payment, payment.amount)
33
+ end
34
+
35
+ it 'voids a payment from an order' do
36
+ payment = create_authorized_payment
37
+ visit_payments_page
38
+
39
+ void_payment
40
+
41
+ expects_page_to_display_successfully_voided_payment(payment)
42
+ expects_payment_to_be_voided_on_stripe(payment)
43
+ end
44
+
45
+ it 'refunds a payment from an order' do
46
+ payment = create_captured_payment
47
+ visit_payments_page
48
+
49
+ refund_payment
50
+
51
+ expects_page_to_display_successfully_refunded_payment(payment)
52
+ expects_payment_to_be_refunded_on_stripe(payment, payment.amount)
53
+ end
54
+
55
+ it 'partially refunds a payment from an order' do
56
+ payment = create_captured_payment
57
+ visit_payments_page
58
+
59
+ refund_reason = create(:refund_reason)
60
+ partial_refund_amount = 25
61
+ partially_refund_payment(refund_reason, partial_refund_amount)
62
+
63
+ expects_page_to_display_successfully_partially_refunded_payment(payment, partial_refund_amount)
64
+ expects_payment_to_be_refunded_on_stripe(payment, partial_refund_amount)
65
+ end
66
+
67
+ it 'cancels an order with captured payment' do
68
+ payment = create_captured_payment
69
+ visit_payments_page
70
+
71
+ cancel_order
72
+
73
+ # https://github.com/solidusio/solidus/blob/ab59d6435239b50db79d73b9a974af057ad56b52/core/app/models/spree/payment_method.rb#L169-L181
74
+ pending "needs to implement try_void method to handle voiding payments on order cancellation"
75
+
76
+ expects_page_to_display_successfully_canceled_order_payment(payment)
77
+ expects_payment_to_be_voided_on_stripe(payment)
78
+ end
79
+
80
+ it 'cancels an order with authorized payment' do
81
+ payment = create_authorized_payment
82
+ visit_payments_page
83
+
84
+ cancel_order
85
+
86
+ # https://github.com/solidusio/solidus/blob/ab59d6435239b50db79d73b9a974af057ad56b52/core/app/models/spree/payment_method.rb#L169-L181
87
+ # Note that for this specific case, the test is pending also because there is an issue
88
+ # with locating the user who canceled the order.
89
+ pending "needs to implement try_void method to handle voiding payments on order cancellation"
90
+
91
+ expects_page_to_display_successfully_canceled_order_payment(payment)
92
+ expects_payment_to_be_voided_on_stripe(payment)
93
+ end
94
+
95
+ it 'creates new authorized payment and captures it with existing source successfully' do
96
+ create_payment_method
97
+ create_order_with_existing_payment_source
98
+ complete_order_with_existing_payment_source
99
+ visit_payments_page
100
+ capture_payment
101
+ payment = last_valid_payment
102
+
103
+ expects_page_to_display_successfully_captured_payment(payment)
104
+ expects_payment_to_have_correct_capture_amount_on_stripe(payment, payment.amount)
105
+ end
106
+ end
107
+
108
+ context 'with failed payment operations' do
109
+ it 'fails to capture a payment due to incomplete 3D Secure authentication' do
110
+ payment = create_authorized_payment(card_number: '4000000000003220')
111
+ visit_payments_page
112
+
113
+ capture_payment
114
+
115
+ expects_page_to_display_capture_fail_message(payment,
116
+ 'This PaymentIntent could not be captured because it has a status of requires_action.')
117
+ end
118
+ end
119
+ end
File without changes