solidus_stripe 4.4.1 → 5.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
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