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