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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +92 -52
- data/.gitignore +3 -0
- data/.rubocop.yml +94 -4
- data/.yardopts +1 -0
- data/Gemfile +10 -30
- data/LICENSE +2 -2
- data/Procfile.dev +3 -0
- data/README.md +145 -215
- data/Rakefile +5 -44
- data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
- data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
- data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
- data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
- data/app/models/solidus_stripe/customer.rb +21 -0
- data/app/models/solidus_stripe/gateway.rb +231 -0
- data/app/models/solidus_stripe/payment_intent.rb +111 -0
- data/app/models/solidus_stripe/payment_method.rb +106 -0
- data/app/models/solidus_stripe/payment_source.rb +31 -0
- data/app/models/solidus_stripe/slug_entry.rb +20 -0
- data/app/models/solidus_stripe.rb +7 -0
- data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
- data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
- data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +14 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
- data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
- data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
- data/bin/dev +13 -0
- data/bin/dummy-app +29 -0
- data/bin/rails +38 -3
- data/bin/rails-dummy-app +3 -0
- data/bin/rails-engine +1 -11
- data/bin/rails-new +55 -0
- data/bin/rails-sandbox +1 -14
- data/bin/rspec +10 -0
- data/bin/sandbox +12 -74
- data/bin/setup +1 -0
- data/bin/update-migrations +56 -0
- data/codecov.yml +12 -0
- data/config/locales/en.yml +16 -1
- data/config/routes.rb +5 -11
- data/coverage.rb +42 -0
- data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
- data/db/migrate/20230303154931_create_solidus_stripe_setup_intent.rb +10 -0
- data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +10 -0
- data/db/migrate/20230308122414_create_solidus_stripe_webhook_endpoints.rb +10 -0
- data/db/migrate/20230310152615_add_payment_method_reference_to_stripe_intents.rb +6 -0
- data/db/migrate/20230310171444_normalize_stripe_intent_id_attributes.rb +6 -0
- data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +16 -0
- data/db/migrate/20230323154931_drop_solidus_stripe_setup_intent.rb +13 -0
- data/db/migrate/20230403094916_rename_webhook_endpoint_to_payment_method_slug_entries.rb +5 -0
- data/db/seeds.rb +6 -24
- data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
- data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +16 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +39 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +20 -0
- data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
- data/lib/solidus_stripe/configuration.rb +24 -3
- data/lib/solidus_stripe/engine.rb +19 -6
- data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
- data/lib/solidus_stripe/refunds_synchronizer.rb +96 -0
- data/lib/solidus_stripe/seeds.rb +19 -0
- data/lib/solidus_stripe/testing_support/factories.rb +153 -0
- data/lib/solidus_stripe/version.rb +1 -1
- data/lib/solidus_stripe/webhook/event.rb +90 -0
- data/lib/solidus_stripe.rb +0 -2
- data/solidus_stripe.gemspec +29 -6
- data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
- data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
- data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
- data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
- data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
- data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
- data/spec/models/solidus_stripe/customer_spec.rb +47 -0
- data/spec/models/solidus_stripe/gateway_spec.rb +283 -0
- data/spec/models/solidus_stripe/payment_intent_spec.rb +17 -0
- data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
- data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
- data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +52 -0
- data/spec/solidus_stripe_spec_helper.rb +10 -0
- data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
- data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
- data/spec/support/solidus_stripe/backend_test_helper.rb +210 -0
- data/spec/support/solidus_stripe/checkout_test_helper.rb +339 -0
- data/spec/support/solidus_stripe/factories.rb +5 -0
- data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
- data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
- data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
- data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +119 -0
- data/spec/system/frontend/.keep +0 -0
- data/spec/system/frontend/solidus_stripe/checkout_spec.rb +187 -0
- data/tmp/.keep +0 -0
- metadata +202 -78
- data/.rubocop_todo.yml +0 -298
- data/.travis.yml +0 -28
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
- data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
- data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
- data/app/controllers/spree/stripe_controller.rb +0 -13
- data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
- data/app/decorators/models/spree/payment_decorator.rb +0 -11
- data/app/decorators/models/spree/refund_decorator.rb +0 -9
- data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
- data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
- data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
- data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
- data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
- data/bin/r +0 -13
- data/bin/sandbox_rails +0 -18
- data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
- data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
- data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
- data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
- data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
- data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
- data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
- data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
- data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
- data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
- data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
- data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
- data/spec/features/stripe_checkout_spec.rb +0 -486
- data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
- data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
- data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
- data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
- data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -354
- data/spec/requests/payment_requests_spec.rb +0 -152
- data/spec/solidus_frontend_app_template.rb +0 -17
- data/spec/spec_helper.rb +0 -37
- 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,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
|