spree_stripe 6.0.0.beta4 → 6.0.0.rc2
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 +114 -13
- data/app/models/spree_stripe/gateway.rb +27 -22
- data/app/models/spree_stripe/payment_sources/bank_transfer.rb +2 -2
- data/app/models/spree_stripe/payout_provider.rb +3 -4
- data/app/models/spree_stripe/units.rb +52 -0
- 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 +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8983341a30de22cd7fa7ad6b6d71aebe323935bfdda5ec4ae7644cd0a268d2e9
|
|
4
|
+
data.tar.gz: b249b9acba1b25c1590c960336ee58130ff94a67eb9daf4b300fd508b354a1d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b334eb2bcb2d3be7b32688b51b5585616b895c22c6c012b8da7f42dad3595b65e35f506b58b30bccffc324fc5b704ceb80cb59199ef42470092534ab9b27ba8f
|
|
7
|
+
data.tar.gz: 8e4737ce51b74cbd937790c27a8bc2befab609b1d7391fa9c1e792dfc9a0d11be5b48f9ccb5f63dabb0c5f56acec41ba084b6db23a264e072702cdc65a8c205b
|
|
@@ -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,19 +35,31 @@ 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
|
+
stripe_amount = SpreeStripe::Units.to_stripe(total, order.currency)
|
|
31
52
|
|
|
32
|
-
raise Spree::Core::GatewayError,
|
|
53
|
+
raise Spree::Core::GatewayError, I18n.t('spree.stripe.payment_session_errors.zero_amount') if stripe_amount.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
|
+
stripe_amount, order,
|
|
39
63
|
payment_method_id: stripe_payment_method_id,
|
|
40
64
|
customer_profile_id: gateway_customer&.profile_id
|
|
41
65
|
)
|
|
@@ -61,18 +85,18 @@ module SpreeStripe
|
|
|
61
85
|
|
|
62
86
|
def update_payment_session(payment_session:, amount: nil, external_data: {})
|
|
63
87
|
attrs = {}
|
|
64
|
-
|
|
88
|
+
stripe_amount = nil
|
|
65
89
|
|
|
66
90
|
if amount.present?
|
|
67
91
|
attrs[:amount] = amount
|
|
68
|
-
|
|
92
|
+
stripe_amount = SpreeStripe::Units.to_stripe(amount, payment_session.currency)
|
|
69
93
|
end
|
|
70
94
|
|
|
71
95
|
stripe_payment_method_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
|
|
72
96
|
|
|
73
97
|
update_payment_intent(
|
|
74
98
|
payment_session.external_id,
|
|
75
|
-
|
|
99
|
+
stripe_amount || SpreeStripe::Units.to_stripe(payment_session.amount, payment_session.currency),
|
|
76
100
|
payment_session.owner,
|
|
77
101
|
stripe_payment_method_id
|
|
78
102
|
)
|
|
@@ -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
|
|
@@ -118,8 +165,8 @@ module SpreeStripe
|
|
|
118
165
|
send_request { |opts| Stripe::PaymentIntent.confirm(payment_intent_id, {}, opts) }
|
|
119
166
|
end
|
|
120
167
|
|
|
121
|
-
def capture_payment_intent(payment_intent_id,
|
|
122
|
-
send_request { |opts| Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture:
|
|
168
|
+
def capture_payment_intent(payment_intent_id, stripe_amount)
|
|
169
|
+
send_request { |opts| Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: stripe_amount }, opts) }
|
|
123
170
|
end
|
|
124
171
|
|
|
125
172
|
def cancel_payment_intent(payment_intent_id)
|
|
@@ -170,11 +217,65 @@ 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
|
-
def create_payment_intent(
|
|
276
|
+
def create_payment_intent(stripe_amount, order, payment_method_id: nil, customer_profile_id: nil)
|
|
176
277
|
payload = {
|
|
177
|
-
amount:
|
|
278
|
+
amount: stripe_amount,
|
|
178
279
|
currency: order.currency,
|
|
179
280
|
customer: customer_profile_id,
|
|
180
281
|
payment_method: payment_method_id,
|
|
@@ -203,10 +304,10 @@ module SpreeStripe
|
|
|
203
304
|
end
|
|
204
305
|
|
|
205
306
|
# Only the fields that can legitimately change while a session is pending.
|
|
206
|
-
def update_payment_intent(payment_intent_id,
|
|
307
|
+
def update_payment_intent(payment_intent_id, stripe_amount, order, payment_method_id = nil)
|
|
207
308
|
protect_from_error do
|
|
208
309
|
payload = {
|
|
209
|
-
amount:
|
|
310
|
+
amount: stripe_amount,
|
|
210
311
|
currency: order.currency,
|
|
211
312
|
customer: fetch_or_create_customer(order: order)&.profile_id,
|
|
212
313
|
payment_method: payment_method_id,
|
|
@@ -3,6 +3,8 @@ module SpreeStripe
|
|
|
3
3
|
# created and confirmed through sessions; core's payment lifecycle then drives
|
|
4
4
|
# capture, refund and cancellation against the resulting intent.
|
|
5
5
|
class Gateway < ::Spree::Gateway
|
|
6
|
+
self.accepts_money_amounts = true
|
|
7
|
+
|
|
6
8
|
include SpreeStripe::Gateway::PaymentSessions
|
|
7
9
|
include SpreeStripe::Gateway::PaymentSetupSessions
|
|
8
10
|
include SpreeStripe::Gateway::Webhooks
|
|
@@ -22,6 +24,14 @@ module SpreeStripe
|
|
|
22
24
|
validates :preferred_secret_key, :preferred_publishable_key, presence: true
|
|
23
25
|
validate :validate_secret_key, unless: -> { Rails.env.test? }, if: -> { preferred_secret_key.present? }
|
|
24
26
|
|
|
27
|
+
def self.logo_url
|
|
28
|
+
'https://spreecommerce.org/docs/images/integrations/logos/stripe.png'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.docs_url
|
|
32
|
+
'https://spreecommerce.org/docs/integrations/payments/stripe'
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
def provider_class
|
|
26
36
|
self.class
|
|
27
37
|
end
|
|
@@ -50,12 +60,10 @@ module SpreeStripe
|
|
|
50
60
|
#
|
|
51
61
|
# @return [Boolean]
|
|
52
62
|
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] }
|
|
63
|
+
return false if previously_new_record?
|
|
57
64
|
|
|
58
|
-
changed
|
|
65
|
+
changed = previously_changed_preference_names
|
|
66
|
+
changed.any? && changed.all? { |key| WEBHOOK_REGISTRATION_PREFERENCES.include?(key) }
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
# Endpoints are registered per Stripe account, so a rotated key leaves the
|
|
@@ -67,10 +75,7 @@ module SpreeStripe
|
|
|
67
75
|
# read as every key having changed.
|
|
68
76
|
return false if previously_new_record?
|
|
69
77
|
|
|
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?
|
|
78
|
+
saved_change_to_preferred_secret_key?
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
def gateway_dashboard_payment_url(payment)
|
|
@@ -83,26 +88,26 @@ module SpreeStripe
|
|
|
83
88
|
# is already confirmed client-side in the session flow, so this reads back the
|
|
84
89
|
# intent rather than initiating a charge.
|
|
85
90
|
#
|
|
86
|
-
# @param
|
|
91
|
+
# @param money [Spree::Money]
|
|
87
92
|
# @param payment_source [Spree::PaymentSource, Spree::CreditCard]
|
|
88
93
|
# @param gateway_options [Hash] Spree::Payment::GatewayOptions#to_hash
|
|
89
94
|
# @return [Spree::PaymentResponse]
|
|
90
|
-
def authorize(
|
|
91
|
-
handle_authorize_or_purchase(
|
|
95
|
+
def authorize(money, payment_source, gateway_options = {})
|
|
96
|
+
handle_authorize_or_purchase(money, payment_source, gateway_options)
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
# @see #authorize — capture vs authorize is decided by the intent's capture
|
|
95
100
|
# method, so both resolve the same way.
|
|
96
|
-
def purchase(
|
|
97
|
-
handle_authorize_or_purchase(
|
|
101
|
+
def purchase(money, payment_source, gateway_options = {})
|
|
102
|
+
handle_authorize_or_purchase(money, payment_source, gateway_options)
|
|
98
103
|
end
|
|
99
104
|
|
|
100
|
-
def capture(
|
|
105
|
+
def capture(money, payment_intent_id, _gateway_options = {})
|
|
101
106
|
protect_from_error do
|
|
102
107
|
stripe_payment_intent = retrieve_payment_intent(payment_intent_id)
|
|
103
108
|
|
|
104
109
|
response = if payment_intent_requires_capture?(stripe_payment_intent)
|
|
105
|
-
capture_payment_intent(payment_intent_id,
|
|
110
|
+
capture_payment_intent(payment_intent_id, SpreeStripe::Units.to_stripe(money, money.currency))
|
|
106
111
|
elsif stripe_payment_intent.status == 'succeeded'
|
|
107
112
|
stripe_payment_intent
|
|
108
113
|
else
|
|
@@ -113,10 +118,10 @@ module SpreeStripe
|
|
|
113
118
|
end
|
|
114
119
|
end
|
|
115
120
|
|
|
116
|
-
def credit(
|
|
121
|
+
def credit(money, _source, payment_intent_id, _gateway_options = {})
|
|
117
122
|
protect_from_error do
|
|
118
123
|
response = send_request do |opts|
|
|
119
|
-
Stripe::Refund.create({ amount:
|
|
124
|
+
Stripe::Refund.create({ amount: SpreeStripe::Units.to_stripe(money, money.currency), payment_intent: payment_intent_id }, opts)
|
|
120
125
|
end
|
|
121
126
|
|
|
122
127
|
success(response.id, response)
|
|
@@ -266,7 +271,7 @@ module SpreeStripe
|
|
|
266
271
|
|
|
267
272
|
private
|
|
268
273
|
|
|
269
|
-
def handle_authorize_or_purchase(
|
|
274
|
+
def handle_authorize_or_purchase(_money, _payment_source, gateway_options)
|
|
270
275
|
# Scoped through this gateway's own payments — a cart-owned payment
|
|
271
276
|
# (checkout is still in flight) has no order to join through, and the
|
|
272
277
|
# payment method already belongs to exactly one store. Found by the
|
|
@@ -302,14 +307,14 @@ module SpreeStripe
|
|
|
302
307
|
def validate_secret_key
|
|
303
308
|
Stripe::Refund.list({ limit: 0 }, api_options)
|
|
304
309
|
rescue Stripe::AuthenticationError
|
|
305
|
-
errors.add(:base, :secret_key_invalid, message:
|
|
310
|
+
errors.add(:base, :secret_key_invalid, message: I18n.t('spree.stripe.errors.secret_key_invalid'))
|
|
306
311
|
rescue Stripe::PermissionError => e
|
|
307
312
|
return unless e.error&.code == 'secret_key_required'
|
|
308
313
|
|
|
309
314
|
errors.add(:base, :publishable_key_provided,
|
|
310
|
-
message:
|
|
315
|
+
message: I18n.t('spree.stripe.errors.publishable_key_provided'))
|
|
311
316
|
rescue Stripe::StripeError
|
|
312
|
-
errors.add(:base, :stripe_unavailable, message:
|
|
317
|
+
errors.add(:base, :stripe_unavailable, message: I18n.t('spree.stripe.errors.stripe_unavailable'))
|
|
313
318
|
end
|
|
314
319
|
|
|
315
320
|
def build_customer_payload(order: nil, customer: nil)
|
|
@@ -258,7 +258,7 @@ module SpreeStripe
|
|
|
258
258
|
wanted = currency.to_s.downcase
|
|
259
259
|
minor = balance.available.select { |entry| entry.currency == wanted }.sum(&:amount)
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
SpreeStripe::Units.from_stripe(minor, currency)
|
|
262
262
|
rescue *AMBIGUOUS_ERRORS => e
|
|
263
263
|
# The sweep must not read silence as "nothing to send" and settle a
|
|
264
264
|
# smaller batch than the seller is owed, nor as "no limit" and ask for
|
|
@@ -290,8 +290,7 @@ module SpreeStripe
|
|
|
290
290
|
return {} if balance_transaction.blank? || balance_transaction.try(:currency).blank?
|
|
291
291
|
|
|
292
292
|
{
|
|
293
|
-
settled_amount:
|
|
294
|
-
balance_transaction.currency),
|
|
293
|
+
settled_amount: SpreeStripe::Units.from_stripe(balance_transaction.amount, balance_transaction.currency),
|
|
295
294
|
settled_currency: balance_transaction.currency.upcase
|
|
296
295
|
}
|
|
297
296
|
end
|
|
@@ -322,7 +321,7 @@ module SpreeStripe
|
|
|
322
321
|
end
|
|
323
322
|
|
|
324
323
|
def minor_units(record)
|
|
325
|
-
|
|
324
|
+
SpreeStripe::Units.to_stripe(record.amount.abs, record.currency, payout: true)
|
|
326
325
|
end
|
|
327
326
|
|
|
328
327
|
# Ties transfers paid from the platform balance to the checkout they
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
# Stripe's integer amounts. Mostly the currency's ISO minor unit, with
|
|
3
|
+
# Stripe's own exceptions (https://docs.stripe.com/currencies): its
|
|
4
|
+
# zero-decimal currencies are sent in whole units, and ISK and UGX, zero-decimal
|
|
5
|
+
# since, are still sent as two-decimal values ending in 00. Stripe also takes
|
|
6
|
+
# three-decimal currencies only in steps of 0.010, and pays out HUF and TWD
|
|
7
|
+
# only in whole units; an amount it would refuse raises here, naming the rule,
|
|
8
|
+
# rather than being rounded to a sum other than the one Spree records.
|
|
9
|
+
module Units
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
ZERO_DECIMAL = %w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF VND VUV XAF XOF XPF].freeze
|
|
13
|
+
WHOLE_IN_HUNDREDTHS = %w[ISK UGX].freeze
|
|
14
|
+
THREE_DECIMAL = %w[BHD JOD KWD OMR TND].freeze
|
|
15
|
+
WHOLE_PAYOUTS = %w[HUF TWD].freeze
|
|
16
|
+
|
|
17
|
+
class UnsupportedAmount < Spree::Core::GatewayError; end
|
|
18
|
+
|
|
19
|
+
# @param amount [BigDecimal, Spree::Money]
|
|
20
|
+
# @param currency [String]
|
|
21
|
+
# @param payout [Boolean] a payout or transfer rather than a charge
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
# @raise [UnsupportedAmount] when Stripe would refuse the amount
|
|
24
|
+
def to_stripe(amount, currency, payout: false)
|
|
25
|
+
amount = amount.to_d if amount.respond_to?(:to_d)
|
|
26
|
+
code = currency.to_s.upcase
|
|
27
|
+
return Spree::Money::Rounding.quantize(amount, 0).to_i if ZERO_DECIMAL.include?(code)
|
|
28
|
+
return Spree::Money::Rounding.quantize(amount, 0).to_i * 100 if WHOLE_IN_HUNDREDTHS.include?(code)
|
|
29
|
+
|
|
30
|
+
units = Spree::Money::Rounding.to_minor_units(amount, code)
|
|
31
|
+
if THREE_DECIMAL.include?(code) && (units % 10).nonzero?
|
|
32
|
+
raise UnsupportedAmount, "Stripe takes #{code} only in steps of 0.010, got #{amount.to_s('F')}"
|
|
33
|
+
end
|
|
34
|
+
if payout && WHOLE_PAYOUTS.include?(code) && (units % 100).nonzero?
|
|
35
|
+
raise UnsupportedAmount, "Stripe pays out #{code} only in whole units, got #{amount.to_s('F')}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
units
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param units [Integer] as Stripe reports it
|
|
42
|
+
# @param currency [String]
|
|
43
|
+
# @return [BigDecimal]
|
|
44
|
+
def from_stripe(units, currency)
|
|
45
|
+
code = currency.to_s.upcase
|
|
46
|
+
return BigDecimal(units) if ZERO_DECIMAL.include?(code)
|
|
47
|
+
return BigDecimal(units) / 100 if WHOLE_IN_HUNDREDTHS.include?(code)
|
|
48
|
+
|
|
49
|
+
Spree::Money::Rounding.from_minor_units(units, code)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
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.rc2
|
|
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-13 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.rc2
|
|
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.rc2
|
|
28
28
|
- !ruby/object:Gem::Dependency
|
|
29
29
|
name: stripe
|
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- app/models/spree_stripe/payment_sources/przelewy24.rb
|
|
90
90
|
- app/models/spree_stripe/payment_sources/sepa_debit.rb
|
|
91
91
|
- app/models/spree_stripe/payout_provider.rb
|
|
92
|
+
- app/models/spree_stripe/units.rb
|
|
92
93
|
- app/presenters/spree_stripe/customer_presenter.rb
|
|
93
94
|
- app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb
|
|
94
95
|
- app/services/spree_stripe/create_gateway_webhooks.rb
|
|
@@ -110,9 +111,9 @@ licenses:
|
|
|
110
111
|
- MIT
|
|
111
112
|
metadata:
|
|
112
113
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
113
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.
|
|
114
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.rc2
|
|
114
115
|
documentation_uri: https://docs.spreecommerce.org/
|
|
115
|
-
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.
|
|
116
|
+
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.rc2
|
|
116
117
|
post_install_message:
|
|
117
118
|
rdoc_options: []
|
|
118
119
|
require_paths:
|