spree_stripe 1.6.0 → 1.7.0
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/payment_setup_sessions/stripe.rb +21 -0
- data/app/models/spree_stripe/gateway/payment_intents.rb +16 -1
- data/app/models/spree_stripe/gateway/payment_sessions.rb +5 -2
- data/app/models/spree_stripe/gateway/payment_setup_sessions.rb +63 -0
- data/app/models/spree_stripe/gateway.rb +12 -9
- data/app/presenters/spree_stripe/payment_intent_presenter.rb +8 -2
- data/app/services/spree_stripe/webhook_handlers/base.rb +19 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_amount_capturable_updated.rb +13 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +3 -8
- data/config/initializers/stripe.rb +1 -0
- data/lib/spree_stripe/configuration.rb +6 -1
- data/lib/spree_stripe/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a10e7ebb4ff5cc5960d0ecb894cfc0e15ef4ed058bf2133fc412ca9fc4c95910
|
|
4
|
+
data.tar.gz: 49fd3295806ed476343813ac3ce3356d74054bf84db8ecf9f4ade86099f139c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ceff451db2064b42fe695aaa91b0ff7c2512fa455504ba459f84a1c788731fd490d85c3a950972a3a7942acaf9cce64a2d6d2de2ce0ace9d2bfccacccc95770
|
|
7
|
+
data.tar.gz: cd0aff0b871a315fd19b5529912ce431ab5c5c35657bab1853fd2548e80591f031f79ee81bab107f64da05f985bfad7271b288fc813a87d4a583935581ad66c0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PaymentSetupSessions::Stripe < PaymentSetupSession
|
|
3
|
+
delegate :api_options, to: :payment_method
|
|
4
|
+
|
|
5
|
+
def stripe_id
|
|
6
|
+
external_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def client_secret
|
|
10
|
+
external_client_secret
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stripe_setup_intent
|
|
14
|
+
@stripe_setup_intent ||= payment_method.retrieve_setup_intent(external_id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def successful?
|
|
18
|
+
stripe_setup_intent.status == 'succeeded'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -5,6 +5,7 @@ module SpreeStripe
|
|
|
5
5
|
|
|
6
6
|
DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = %w[sepa_debit us_bank_account].freeze
|
|
7
7
|
BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
|
|
8
|
+
MANUAL_CAPTURE_METHOD = 'manual'.freeze
|
|
8
9
|
|
|
9
10
|
included do
|
|
10
11
|
has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', foreign_key: 'payment_method_id', dependent: :delete_all
|
|
@@ -32,6 +33,14 @@ module SpreeStripe
|
|
|
32
33
|
payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
def payment_intent_requires_capture?(payment_intent)
|
|
37
|
+
payment_intent.status == 'requires_capture'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def payment_intent_manual_capture?(payment_intent)
|
|
41
|
+
payment_intent.respond_to?(:capture_method) && payment_intent.capture_method == MANUAL_CAPTURE_METHOD
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
# Creates a Stripe payment intent for the order
|
|
36
45
|
#
|
|
37
46
|
# @param amount_in_cents [Integer] the amount in cents
|
|
@@ -46,7 +55,8 @@ module SpreeStripe
|
|
|
46
55
|
order: order,
|
|
47
56
|
customer: customer_profile_id || fetch_or_create_customer(order: order)&.profile_id,
|
|
48
57
|
payment_method_id: payment_method_id,
|
|
49
|
-
off_session: off_session
|
|
58
|
+
off_session: off_session,
|
|
59
|
+
capture_method: stripe_capture_method
|
|
50
60
|
).call
|
|
51
61
|
|
|
52
62
|
protect_from_error do
|
|
@@ -127,8 +137,13 @@ module SpreeStripe
|
|
|
127
137
|
|
|
128
138
|
private
|
|
129
139
|
|
|
140
|
+
def stripe_capture_method
|
|
141
|
+
auto_capture? ? nil : MANUAL_CAPTURE_METHOD
|
|
142
|
+
end
|
|
143
|
+
|
|
130
144
|
def payment_intent_accepted_statuses(payment_intent)
|
|
131
145
|
statuses = %w[succeeded]
|
|
146
|
+
statuses << 'requires_capture' if payment_intent_manual_capture?(payment_intent)
|
|
132
147
|
statuses << 'processing' if payment_intent_delayed_notification?(payment_intent)
|
|
133
148
|
statuses << 'requires_action' if payment_intent_charge_not_required?(payment_intent)
|
|
134
149
|
statuses
|
|
@@ -90,7 +90,8 @@ module SpreeStripe
|
|
|
90
90
|
# Create the Payment record
|
|
91
91
|
payment_session.find_or_create_payment!
|
|
92
92
|
|
|
93
|
-
#
|
|
93
|
+
# `else` covers requires_capture (manual capture), processing (delayed-notification
|
|
94
|
+
# banks), and requires_action (bank transfer awaiting funds) — all auth-only states.
|
|
94
95
|
payment = payment_session.payment
|
|
95
96
|
if payment.present? && !payment.completed?
|
|
96
97
|
if payment_intent_successful?(stripe_pi)
|
|
@@ -128,7 +129,9 @@ module SpreeStripe
|
|
|
128
129
|
return if order.bill_address.present? && order.bill_address.valid?
|
|
129
130
|
|
|
130
131
|
country_iso = address.country
|
|
131
|
-
country = Spree::Country.
|
|
132
|
+
country = (country_iso.present? && Spree::Country.by_iso(country_iso)) ||
|
|
133
|
+
order.store.default_market&.default_country ||
|
|
134
|
+
Spree::Country.by_iso('US')
|
|
132
135
|
|
|
133
136
|
order.bill_address ||= Spree::Address.new(country: country, user: order.user)
|
|
134
137
|
order.bill_address.quick_checkout = true
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentSetupSessions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def setup_session_supported?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def payment_setup_session_class
|
|
11
|
+
Spree::PaymentSetupSessions::Stripe
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_payment_setup_session(customer:, external_data: {})
|
|
15
|
+
gateway_customer = fetch_or_create_customer(user: customer)
|
|
16
|
+
|
|
17
|
+
setup_intent_response = create_setup_intent(gateway_customer.profile_id)
|
|
18
|
+
ephemeral_key_response = create_ephemeral_key(gateway_customer.profile_id)
|
|
19
|
+
|
|
20
|
+
payment_setup_session_class.create!(
|
|
21
|
+
customer: customer,
|
|
22
|
+
payment_method: self,
|
|
23
|
+
status: 'pending',
|
|
24
|
+
external_id: setup_intent_response.params['id'],
|
|
25
|
+
external_client_secret: setup_intent_response.authorization,
|
|
26
|
+
external_data: external_data.to_h.stringify_keys.merge(
|
|
27
|
+
'customer_id' => gateway_customer.profile_id,
|
|
28
|
+
'ephemeral_key_secret' => ephemeral_key_response&.params&.dig('secret')
|
|
29
|
+
).compact
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def complete_payment_setup_session(setup_session:, params: {})
|
|
34
|
+
stripe_setup_intent = retrieve_setup_intent(setup_session.external_id)
|
|
35
|
+
|
|
36
|
+
if stripe_setup_intent.status == 'succeeded'
|
|
37
|
+
setup_session.process if setup_session.can_process?
|
|
38
|
+
|
|
39
|
+
stripe_payment_method = stripe_setup_intent.payment_method
|
|
40
|
+
|
|
41
|
+
source = SpreeStripe::CreateSource.new(
|
|
42
|
+
stripe_payment_method_details: stripe_payment_method,
|
|
43
|
+
stripe_payment_method_id: stripe_payment_method.id,
|
|
44
|
+
stripe_billing_details: stripe_payment_method.billing_details,
|
|
45
|
+
gateway: self,
|
|
46
|
+
user: setup_session.customer
|
|
47
|
+
).call
|
|
48
|
+
|
|
49
|
+
setup_session.payment_source = source
|
|
50
|
+
setup_session.complete unless setup_session.completed?
|
|
51
|
+
else
|
|
52
|
+
setup_session.fail if setup_session.can_fail?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
setup_session
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def retrieve_setup_intent(setup_intent_id)
|
|
59
|
+
send_request { |opts| Stripe::SetupIntent.retrieve({ id: setup_intent_id, expand: ['payment_method'] }, opts) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -2,11 +2,18 @@ module SpreeStripe
|
|
|
2
2
|
class Gateway < ::Spree::Gateway
|
|
3
3
|
include SpreeStripe::Gateway::PaymentIntents
|
|
4
4
|
include SpreeStripe::Gateway::PaymentSessions
|
|
5
|
+
include SpreeStripe::Gateway::PaymentSetupSessions
|
|
5
6
|
include SpreeStripe::Gateway::Tax if defined?(SpreeStripe::Gateway::Tax)
|
|
6
7
|
|
|
7
8
|
preference :publishable_key, :password
|
|
8
9
|
preference :secret_key, :password
|
|
9
10
|
|
|
11
|
+
WEBHOOK_EVENT_ACTIONS = {
|
|
12
|
+
'payment_intent.succeeded' => :captured,
|
|
13
|
+
'payment_intent.amount_capturable_updated' => :authorized,
|
|
14
|
+
'payment_intent.payment_failed' => :failed
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
10
17
|
has_one_attached :apple_developer_merchantid_domain_association, service: Spree.private_storage_service_name
|
|
11
18
|
|
|
12
19
|
validates :preferred_secret_key, :preferred_publishable_key, presence: true
|
|
@@ -29,20 +36,16 @@ module SpreeStripe
|
|
|
29
36
|
def parse_webhook_event(raw_body, headers)
|
|
30
37
|
event = verify_webhook_signature(raw_body, headers)
|
|
31
38
|
|
|
39
|
+
action = WEBHOOK_EVENT_ACTIONS[event.type]
|
|
40
|
+
return nil unless action
|
|
41
|
+
|
|
32
42
|
payment_session = Spree::PaymentSessions::Stripe.find_by(
|
|
33
43
|
payment_method: self,
|
|
34
44
|
external_id: event.data.object[:id]
|
|
35
45
|
)
|
|
36
46
|
return nil unless payment_session
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
when 'payment_intent.succeeded'
|
|
40
|
-
{ action: :captured, payment_session: payment_session, metadata: { stripe_event: event } }
|
|
41
|
-
when 'payment_intent.payment_failed'
|
|
42
|
-
{ action: :failed, payment_session: payment_session, metadata: { stripe_event: event } }
|
|
43
|
-
else
|
|
44
|
-
nil
|
|
45
|
-
end
|
|
48
|
+
{ action: action, payment_session: payment_session, metadata: { stripe_event: event } }
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def provider_class
|
|
@@ -106,7 +109,7 @@ module SpreeStripe
|
|
|
106
109
|
protect_from_error do
|
|
107
110
|
stripe_payment_intent = retrieve_payment_intent(payment_intent_id)
|
|
108
111
|
|
|
109
|
-
response = if stripe_payment_intent
|
|
112
|
+
response = if payment_intent_requires_capture?(stripe_payment_intent)
|
|
110
113
|
capture_payment_intent(payment_intent_id, amount_in_cents)
|
|
111
114
|
elsif stripe_payment_intent.status == 'succeeded'
|
|
112
115
|
stripe_payment_intent
|
|
@@ -2,13 +2,14 @@ module SpreeStripe
|
|
|
2
2
|
class PaymentIntentPresenter
|
|
3
3
|
SETUP_FUTURE_USAGE = 'off_session'
|
|
4
4
|
|
|
5
|
-
def initialize(amount:, order:, customer: nil, payment_method_id: nil, off_session: false)
|
|
5
|
+
def initialize(amount:, order:, customer: nil, payment_method_id: nil, off_session: false, capture_method: nil)
|
|
6
6
|
@amount = amount
|
|
7
7
|
@order = order
|
|
8
8
|
@customer = customer
|
|
9
9
|
@ship_address = order.ship_address
|
|
10
10
|
@payment_method_id = payment_method_id
|
|
11
11
|
@off_session = off_session
|
|
12
|
+
@capture_method = capture_method
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def call
|
|
@@ -19,6 +20,7 @@ module SpreeStripe
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
payload = payload.deep_merge(basic_payload)
|
|
23
|
+
payload = payload.merge(capture_method: SpreeStripe::Gateway::PaymentIntents::MANUAL_CAPTURE_METHOD) if manual_capture?
|
|
22
24
|
|
|
23
25
|
return payload unless ship_address
|
|
24
26
|
|
|
@@ -49,7 +51,11 @@ module SpreeStripe
|
|
|
49
51
|
|
|
50
52
|
private
|
|
51
53
|
|
|
52
|
-
attr_reader :order, :amount, :customer, :ship_address, :payment_method_id
|
|
54
|
+
attr_reader :order, :amount, :customer, :ship_address, :payment_method_id, :capture_method
|
|
55
|
+
|
|
56
|
+
def manual_capture?
|
|
57
|
+
capture_method.to_s == SpreeStripe::Gateway::PaymentIntents::MANUAL_CAPTURE_METHOD
|
|
58
|
+
end
|
|
53
59
|
|
|
54
60
|
def basic_payload
|
|
55
61
|
{
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
module WebhookHandlers
|
|
3
|
+
class Base
|
|
4
|
+
# Delay before processing a webhook to give the storefront's own session-complete
|
|
5
|
+
# request a chance to land first, avoiding double-processing of the same payment.
|
|
6
|
+
ENQUEUE_DELAY = 10.seconds
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def enqueue_complete_order_from_session(stripe_id)
|
|
11
|
+
payment_session = Spree::PaymentSessions::Stripe.find_by(external_id: stripe_id)
|
|
12
|
+
return nil if payment_session.nil?
|
|
13
|
+
|
|
14
|
+
SpreeStripe::CompleteOrderFromSessionJob.set(wait: ENQUEUE_DELAY).perform_later(payment_session.id)
|
|
15
|
+
payment_session
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeStripe
|
|
2
|
+
module WebhookHandlers
|
|
3
|
+
# Fires when a manual-capture PaymentIntent transitions to `requires_capture`
|
|
4
|
+
# (i.e. the funds have been authorized but not yet captured). For new
|
|
5
|
+
# PaymentSession-based flows we hand off to CompleteOrderFromSessionJob,
|
|
6
|
+
# which authorizes the Spree::Payment and completes the order.
|
|
7
|
+
class PaymentIntentAmountCapturableUpdated < Base
|
|
8
|
+
def call(event)
|
|
9
|
+
enqueue_complete_order_from_session(event.data.object[:id])
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
module SpreeStripe
|
|
2
2
|
module WebhookHandlers
|
|
3
|
-
class PaymentIntentSucceeded
|
|
3
|
+
class PaymentIntentSucceeded < Base
|
|
4
4
|
def call(event)
|
|
5
5
|
stripe_id = event.data.object[:id]
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
payment_session = Spree::PaymentSessions::Stripe.find_by(external_id: stripe_id)
|
|
9
|
-
if payment_session.present?
|
|
10
|
-
SpreeStripe::CompleteOrderFromSessionJob.set(wait: 10.seconds).perform_later(payment_session.id)
|
|
11
|
-
return
|
|
12
|
-
end
|
|
7
|
+
return if enqueue_complete_order_from_session(stripe_id)
|
|
13
8
|
|
|
14
9
|
# Legacy system: PaymentIntent
|
|
15
10
|
payment_intent = SpreeStripe::PaymentIntent.find_by(stripe_id: stripe_id)
|
|
16
11
|
return if payment_intent.nil?
|
|
17
12
|
|
|
18
|
-
SpreeStripe::CompleteOrderJob.set(wait:
|
|
13
|
+
SpreeStripe::CompleteOrderJob.set(wait: ENQUEUE_DELAY).perform_later(payment_intent.id)
|
|
19
14
|
end
|
|
20
15
|
end
|
|
21
16
|
end
|
|
@@ -8,6 +8,7 @@ Stripe.set_app_info('Spree Stripe', version: Spree.version, url: 'https://spreec
|
|
|
8
8
|
Rails.application.config.after_initialize do
|
|
9
9
|
StripeEvent.configure do |events|
|
|
10
10
|
events.subscribe 'payment_intent.succeeded', SpreeStripe::WebhookHandlers::PaymentIntentSucceeded.new
|
|
11
|
+
events.subscribe 'payment_intent.amount_capturable_updated', SpreeStripe::WebhookHandlers::PaymentIntentAmountCapturableUpdated.new
|
|
11
12
|
events.subscribe 'payment_intent.payment_failed', SpreeStripe::WebhookHandlers::PaymentIntentPaymentFailed.new
|
|
12
13
|
events.subscribe 'setup_intent.succeeded', SpreeStripe::WebhookHandlers::SetupIntentSucceeded.new
|
|
13
14
|
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
module SpreeStripe
|
|
2
2
|
class Configuration < Spree::Preferences::Configuration
|
|
3
|
-
preference :supported_webhook_events, :array, default: %w[
|
|
3
|
+
preference :supported_webhook_events, :array, default: %w[
|
|
4
|
+
payment_intent.amount_capturable_updated
|
|
5
|
+
payment_intent.payment_failed
|
|
6
|
+
payment_intent.succeeded
|
|
7
|
+
setup_intent.succeeded
|
|
8
|
+
]
|
|
4
9
|
preference :use_legacy_payment_intents, :boolean, default: false
|
|
5
10
|
preference :use_legacy_webhook_handlers, :boolean, default: false
|
|
6
11
|
end
|
data/lib/spree_stripe/version.rb
CHANGED
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: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vendo Connect Inc., Vendo Sp. z o.o.
|
|
@@ -184,6 +184,7 @@ files:
|
|
|
184
184
|
- app/jobs/spree_stripe/register_domain_job.rb
|
|
185
185
|
- app/jobs/spree_stripe/update_customer_job.rb
|
|
186
186
|
- app/models/spree/payment_sessions/stripe.rb
|
|
187
|
+
- app/models/spree/payment_setup_sessions/stripe.rb
|
|
187
188
|
- app/models/spree_stripe/base.rb
|
|
188
189
|
- app/models/spree_stripe/calculators/stripe_tax.rb
|
|
189
190
|
- app/models/spree_stripe/credit_card_decorator.rb
|
|
@@ -191,6 +192,7 @@ files:
|
|
|
191
192
|
- app/models/spree_stripe/gateway.rb
|
|
192
193
|
- app/models/spree_stripe/gateway/payment_intents.rb
|
|
193
194
|
- app/models/spree_stripe/gateway/payment_sessions.rb
|
|
195
|
+
- app/models/spree_stripe/gateway/payment_setup_sessions.rb
|
|
194
196
|
- app/models/spree_stripe/gateway_customer_decorator.rb
|
|
195
197
|
- app/models/spree_stripe/order_decorator.rb
|
|
196
198
|
- app/models/spree_stripe/payment_decorator.rb
|
|
@@ -223,6 +225,8 @@ files:
|
|
|
223
225
|
- app/services/spree_stripe/create_source.rb
|
|
224
226
|
- app/services/spree_stripe/register_domain.rb
|
|
225
227
|
- app/services/spree_stripe/update_customer.rb
|
|
228
|
+
- app/services/spree_stripe/webhook_handlers/base.rb
|
|
229
|
+
- app/services/spree_stripe/webhook_handlers/payment_intent_amount_capturable_updated.rb
|
|
226
230
|
- app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb
|
|
227
231
|
- app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb
|
|
228
232
|
- app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb
|
|
@@ -284,9 +288,9 @@ licenses:
|
|
|
284
288
|
- MIT
|
|
285
289
|
metadata:
|
|
286
290
|
bug_tracker_uri: https://github.com/spree/spree_stripe/issues
|
|
287
|
-
changelog_uri: https://github.com/spree/spree_stripe/releases/tag/v1.
|
|
291
|
+
changelog_uri: https://github.com/spree/spree_stripe/releases/tag/v1.7.0
|
|
288
292
|
documentation_uri: https://docs.spreecommerce.org/
|
|
289
|
-
source_code_uri: https://github.com/spree/spree_stripe/tree/v1.
|
|
293
|
+
source_code_uri: https://github.com/spree/spree_stripe/tree/v1.7.0
|
|
290
294
|
rdoc_options: []
|
|
291
295
|
require_paths:
|
|
292
296
|
- lib
|