spree_stripe 6.0.0.beta4 → 6.0.0.rc1
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/app/models/spree_stripe/gateway/payment_sessions.rb +103 -2
- data/app/models/spree_stripe/gateway.rb +15 -12
- data/app/models/spree_stripe/payment_sources/bank_transfer.rb +2 -2
- data/config/locales/en.yml +1 -1
- data/lib/spree_stripe/engine.rb +0 -6
- data/lib/tasks/migrate_webhook_keys.rake +7 -4
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b65629d2703ce9659457a090c9baf88ef3f7685550a64e6a8c8e3c296531ad15
|
|
4
|
+
data.tar.gz: a82e689658e39723dd178006b7937793f128fbff6436a48f1e4ddb2e4f74eed1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5bb7beea57c85809530e0601431e0292943b18636cbaeecffeba60b73cd1493ec13b9b825af477462eed2022b5ae85410461c5259b7258768121c38d451f6b3
|
|
7
|
+
data.tar.gz: 5972312842ff35f2ca34fdfdded24cd3f95ea89146951536ae9b53b8111013f6bfba70480cc399cd18b4940153f64651046aa998eac0c61b27189165c19016a4
|
|
@@ -15,6 +15,18 @@ module SpreeStripe
|
|
|
15
15
|
MANUAL_CAPTURE_METHOD = 'manual'.freeze
|
|
16
16
|
SETUP_FUTURE_USAGE = 'off_session'.freeze
|
|
17
17
|
|
|
18
|
+
# Not a decision about the intent. A timeout and an outage may not have
|
|
19
|
+
# arrived; a rate limit was turned away before Stripe looked; a bad key
|
|
20
|
+
# or a missing permission never reached the intent. A refusal of the
|
|
21
|
+
# cancel itself is an InvalidRequestError and stays out of this list.
|
|
22
|
+
RETRYABLE_STRIPE_ERRORS = [
|
|
23
|
+
Stripe::APIConnectionError,
|
|
24
|
+
Stripe::RateLimitError,
|
|
25
|
+
Stripe::APIError,
|
|
26
|
+
Stripe::AuthenticationError,
|
|
27
|
+
Stripe::PermissionError
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
18
30
|
def session_required?
|
|
19
31
|
true
|
|
20
32
|
end
|
|
@@ -23,17 +35,29 @@ module SpreeStripe
|
|
|
23
35
|
Spree::PaymentSessions::Stripe
|
|
24
36
|
end
|
|
25
37
|
|
|
38
|
+
# Opens a new session on every call. Each earlier session that can still
|
|
39
|
+
# be paid is canceled first: every open intent can be paid on its own,
|
|
40
|
+
# so a reloaded checkout would charge the buyer twice. A session Stripe
|
|
41
|
+
# has already accepted, a card charge still in flight, or one this cart
|
|
42
|
+
# has already completed is left as it is and the request fails, because
|
|
43
|
+
# that money is not dropped to make room for another session.
|
|
44
|
+
#
|
|
26
45
|
# @param order [Spree::Cart, Spree::Order]
|
|
27
46
|
# @return [Spree::PaymentSessions::Stripe]
|
|
47
|
+
# @raise [Spree::Core::GatewayError] when a session has already been
|
|
48
|
+
# paid, or an earlier one could not be canceled
|
|
28
49
|
def create_payment_session(order:, amount: nil, external_data: {})
|
|
29
50
|
total = amount.presence || order.total_minus_store_credits
|
|
30
51
|
amount_in_cents = Spree::Money.new(total, currency: order.currency).cents
|
|
31
52
|
|
|
32
|
-
raise Spree::Core::GatewayError,
|
|
53
|
+
raise Spree::Core::GatewayError, I18n.t('spree.stripe.payment_session_errors.zero_amount') if amount_in_cents.zero?
|
|
33
54
|
|
|
34
|
-
gateway_customer = fetch_or_create_customer(order: order)
|
|
35
55
|
stripe_payment_method_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
|
|
36
56
|
|
|
57
|
+
replace_open_sessions(order)
|
|
58
|
+
|
|
59
|
+
gateway_customer = fetch_or_create_customer(order: order)
|
|
60
|
+
|
|
37
61
|
response = create_payment_intent(
|
|
38
62
|
amount_in_cents, order,
|
|
39
63
|
payment_method_id: stripe_payment_method_id,
|
|
@@ -110,6 +134,29 @@ module SpreeStripe
|
|
|
110
134
|
payment_session
|
|
111
135
|
end
|
|
112
136
|
|
|
137
|
+
# Stripe refuses once money has moved, and that refusal is raised so the
|
|
138
|
+
# session is never shown as canceled while its money stands. An intent
|
|
139
|
+
# already canceled at Stripe, or unknown to the account the keys now
|
|
140
|
+
# reach, can no longer be paid, so its session is canceled all the same.
|
|
141
|
+
# A timeout, a rate limit, an outage, or rejected credentials is raised
|
|
142
|
+
# as Stripe's own error: wrapped as a refusal, the unused-session job
|
|
143
|
+
# would count the intent handled while the buyer can still pay it.
|
|
144
|
+
#
|
|
145
|
+
# @param payment_session [Spree::PaymentSessions::Stripe]
|
|
146
|
+
# @return [Boolean] whether the session was canceled
|
|
147
|
+
# @raise [Spree::Core::GatewayError] when Stripe refuses
|
|
148
|
+
# @raise [Stripe::StripeError] when Stripe could not be asked
|
|
149
|
+
def cancel_payment_session(payment_session:)
|
|
150
|
+
cancel_payment_intent(payment_session.external_id)
|
|
151
|
+
payment_session.cancel
|
|
152
|
+
rescue *RETRYABLE_STRIPE_ERRORS
|
|
153
|
+
raise
|
|
154
|
+
rescue Stripe::StripeError => error
|
|
155
|
+
raise Spree::Core::GatewayError, error.message unless payment_intent_unpayable?(payment_session.external_id)
|
|
156
|
+
|
|
157
|
+
payment_session.cancel
|
|
158
|
+
end
|
|
159
|
+
|
|
113
160
|
def retrieve_payment_intent(payment_intent_id)
|
|
114
161
|
send_request { |opts| Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }, opts) }
|
|
115
162
|
end
|
|
@@ -170,6 +217,60 @@ module SpreeStripe
|
|
|
170
217
|
|
|
171
218
|
private
|
|
172
219
|
|
|
220
|
+
# Nothing is canceled until every still-payable intent has been read. A
|
|
221
|
+
# later one that Stripe has already accepted must not be reached after
|
|
222
|
+
# an earlier one was dropped. A completed session is the same refusal
|
|
223
|
+
# with no provider call: the cart already has its payment.
|
|
224
|
+
def replace_open_sessions(order)
|
|
225
|
+
sessions = order.payment_sessions.where(payment_method: self)
|
|
226
|
+
if sessions.exists?(status: 'completed')
|
|
227
|
+
raise Spree::Core::GatewayError, I18n.t('spree.stripe.payment_session_errors.payment_in_progress')
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A failed confirm leaves the intent payable. It is canceled with the
|
|
231
|
+
# open ones, unless its charge is still in flight.
|
|
232
|
+
open_sessions = sessions.active.order(:id).to_a + sessions.where(status: 'failed').order(:id).to_a
|
|
233
|
+
return if open_sessions.empty?
|
|
234
|
+
|
|
235
|
+
open_sessions.each do |payment_session|
|
|
236
|
+
payment_intent = find_payment_intent(payment_session.external_id)
|
|
237
|
+
next unless payment_intent && payment_intent_in_progress?(payment_intent)
|
|
238
|
+
|
|
239
|
+
raise Spree::Core::GatewayError, I18n.t('spree.stripe.payment_session_errors.payment_in_progress')
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
open_sessions.each do |payment_session|
|
|
243
|
+
cancel_payment_session(payment_session: payment_session)
|
|
244
|
+
end
|
|
245
|
+
rescue *RETRYABLE_STRIPE_ERRORS => error
|
|
246
|
+
# Checkout speaks GatewayError. The unused-session job does not: it
|
|
247
|
+
# retries these, and has to see them unwrapped.
|
|
248
|
+
raise Spree::Core::GatewayError, error.message
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# A card can sit in `processing` while its charge is in flight.
|
|
252
|
+
# `payment_intent_accepted?` does not record that yet, and canceling
|
|
253
|
+
# the intent can drop or refund the charge.
|
|
254
|
+
def payment_intent_in_progress?(payment_intent)
|
|
255
|
+
payment_intent.status == 'processing' || payment_intent_accepted?(payment_intent)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# nil when the account the keys now reach has no such intent.
|
|
259
|
+
def find_payment_intent(payment_intent_id)
|
|
260
|
+
retrieve_payment_intent(payment_intent_id)
|
|
261
|
+
rescue Stripe::InvalidRequestError => error
|
|
262
|
+
raise Spree::Core::GatewayError, error.message unless error.code == 'resource_missing'
|
|
263
|
+
rescue *RETRYABLE_STRIPE_ERRORS
|
|
264
|
+
raise
|
|
265
|
+
rescue Stripe::StripeError => error
|
|
266
|
+
raise Spree::Core::GatewayError, error.message
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def payment_intent_unpayable?(payment_intent_id)
|
|
270
|
+
payment_intent = find_payment_intent(payment_intent_id)
|
|
271
|
+
payment_intent.nil? || payment_intent.status == 'canceled'
|
|
272
|
+
end
|
|
273
|
+
|
|
173
274
|
# @param order [Spree::Cart, Spree::Order]
|
|
174
275
|
# @return [Spree::PaymentResponse]
|
|
175
276
|
def create_payment_intent(amount_in_cents, order, payment_method_id: nil, customer_profile_id: nil)
|
|
@@ -22,6 +22,14 @@ module SpreeStripe
|
|
|
22
22
|
validates :preferred_secret_key, :preferred_publishable_key, presence: true
|
|
23
23
|
validate :validate_secret_key, unless: -> { Rails.env.test? }, if: -> { preferred_secret_key.present? }
|
|
24
24
|
|
|
25
|
+
def self.logo_url
|
|
26
|
+
'https://spreecommerce.org/docs/images/integrations/logos/stripe.png'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.docs_url
|
|
30
|
+
'https://spreecommerce.org/docs/integrations/payments/stripe'
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
def provider_class
|
|
26
34
|
self.class
|
|
27
35
|
end
|
|
@@ -50,12 +58,10 @@ module SpreeStripe
|
|
|
50
58
|
#
|
|
51
59
|
# @return [Boolean]
|
|
52
60
|
def only_webhook_registration_changed?
|
|
53
|
-
|
|
54
|
-
return false unless before.is_a?(Hash) && after.is_a?(Hash)
|
|
55
|
-
|
|
56
|
-
changed = (before.keys | after.keys).reject { |key| before[key] == after[key] }
|
|
61
|
+
return false if previously_new_record?
|
|
57
62
|
|
|
58
|
-
changed
|
|
63
|
+
changed = previously_changed_preference_names
|
|
64
|
+
changed.any? && changed.all? { |key| WEBHOOK_REGISTRATION_PREFERENCES.include?(key) }
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
# Endpoints are registered per Stripe account, so a rotated key leaves the
|
|
@@ -67,10 +73,7 @@ module SpreeStripe
|
|
|
67
73
|
# read as every key having changed.
|
|
68
74
|
return false if previously_new_record?
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
# reads the change through indifferent access, so it answers the same
|
|
72
|
-
# whether the serialized hash came back with symbol or string keys.
|
|
73
|
-
preferred_secret_key_previously_changed?
|
|
76
|
+
saved_change_to_preferred_secret_key?
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def gateway_dashboard_payment_url(payment)
|
|
@@ -302,14 +305,14 @@ module SpreeStripe
|
|
|
302
305
|
def validate_secret_key
|
|
303
306
|
Stripe::Refund.list({ limit: 0 }, api_options)
|
|
304
307
|
rescue Stripe::AuthenticationError
|
|
305
|
-
errors.add(:base, :secret_key_invalid, message:
|
|
308
|
+
errors.add(:base, :secret_key_invalid, message: I18n.t('spree.stripe.errors.secret_key_invalid'))
|
|
306
309
|
rescue Stripe::PermissionError => e
|
|
307
310
|
return unless e.error&.code == 'secret_key_required'
|
|
308
311
|
|
|
309
312
|
errors.add(:base, :publishable_key_provided,
|
|
310
|
-
message:
|
|
313
|
+
message: I18n.t('spree.stripe.errors.publishable_key_provided'))
|
|
311
314
|
rescue Stripe::StripeError
|
|
312
|
-
errors.add(:base, :stripe_unavailable, message:
|
|
315
|
+
errors.add(:base, :stripe_unavailable, message: I18n.t('spree.stripe.errors.stripe_unavailable'))
|
|
313
316
|
end
|
|
314
317
|
|
|
315
318
|
def build_customer_payload(order: nil, customer: nil)
|
data/config/locales/en.yml
CHANGED
|
@@ -7,6 +7,6 @@ en:
|
|
|
7
7
|
publishable_key_provided: You have provided your publishable key instead of your secret key
|
|
8
8
|
secret_key_invalid: Secret key is invalid
|
|
9
9
|
stripe_unavailable: Something went wrong with Stripe. Try again later.
|
|
10
|
-
payment_could_not_be_processed: Your payment could not be processed. Please try again.
|
|
11
10
|
payment_session_errors:
|
|
11
|
+
payment_in_progress: A payment for this checkout is already in progress.
|
|
12
12
|
zero_amount: Payment session amount must be greater than zero
|
data/lib/spree_stripe/engine.rb
CHANGED
|
@@ -5,12 +5,6 @@ module SpreeStripe
|
|
|
5
5
|
isolate_namespace Spree
|
|
6
6
|
engine_name 'spree_stripe'
|
|
7
7
|
|
|
8
|
-
config.paths.add 'app/subscribers', eager_load: true
|
|
9
|
-
|
|
10
|
-
config.generators do |g|
|
|
11
|
-
g.test_framework :rspec
|
|
12
|
-
end
|
|
13
|
-
|
|
14
8
|
# Core assigns the payment method registry in its own after_initialize, so
|
|
15
9
|
# appending has to happen in a later one — engine callbacks run in load order.
|
|
16
10
|
config.after_initialize do
|
|
@@ -40,11 +40,14 @@ module SpreeStripe
|
|
|
40
40
|
next
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Through the preference writers, so the signing secret lands in the
|
|
44
|
+
# encrypted column; saved without callbacks, which would re-register
|
|
45
|
+
# the endpoint the secret belongs to.
|
|
46
|
+
gateway.preferred_webhook_endpoint_id = key.stripe_id
|
|
47
|
+
gateway.preferred_webhook_signing_secret = key.signing_secret
|
|
43
48
|
gateway.update_columns(
|
|
44
|
-
preferences: gateway.preferences
|
|
45
|
-
|
|
46
|
-
webhook_signing_secret: key.signing_secret
|
|
47
|
-
),
|
|
49
|
+
preferences: gateway.preferences,
|
|
50
|
+
secret_preferences: gateway.secret_preferences,
|
|
48
51
|
updated_at: Time.current
|
|
49
52
|
)
|
|
50
53
|
migrated += 1
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_stripe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.0.
|
|
4
|
+
version: 6.0.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vendo Connect Inc.
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: spree_core
|
|
@@ -17,14 +17,14 @@ dependencies:
|
|
|
17
17
|
requirements:
|
|
18
18
|
- - '='
|
|
19
19
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: 6.0.0.
|
|
20
|
+
version: 6.0.0.rc1
|
|
21
21
|
type: :runtime
|
|
22
22
|
prerelease: false
|
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
24
|
requirements:
|
|
25
25
|
- - '='
|
|
26
26
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: 6.0.0.
|
|
27
|
+
version: 6.0.0.rc1
|
|
28
28
|
- !ruby/object:Gem::Dependency
|
|
29
29
|
name: stripe
|
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -110,9 +110,9 @@ licenses:
|
|
|
110
110
|
- MIT
|
|
111
111
|
metadata:
|
|
112
112
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
113
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.
|
|
113
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.rc1
|
|
114
114
|
documentation_uri: https://docs.spreecommerce.org/
|
|
115
|
-
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.
|
|
115
|
+
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.rc1
|
|
116
116
|
post_install_message:
|
|
117
117
|
rdoc_options: []
|
|
118
118
|
require_paths:
|