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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b3cc636d33d9aa8152aba43d60a15a1d3c44dca2feeafcd30c215cec823c028
4
- data.tar.gz: 82cf337690f6c7f03d4770e2001e4d4dc3a273d4b84887f079e664a02788edd6
3
+ metadata.gz: a10e7ebb4ff5cc5960d0ecb894cfc0e15ef4ed058bf2133fc412ca9fc4c95910
4
+ data.tar.gz: 49fd3295806ed476343813ac3ce3356d74054bf84db8ecf9f4ade86099f139c3
5
5
  SHA512:
6
- metadata.gz: 0ddd1b76f5c7991a636f78751fc43c8bc5eac6b952a91621ad928eaa66823f0b2bfa42a21ba7ba2942934c9146eb362841ad8ef7cc74f63b1422f8a2fbdf23ec
7
- data.tar.gz: 9b9d768391f1e41ff1444dcf9f6faad7bab6c73c25e9712f0628171d770c3d00ef9b71d38f8be1ab471a21e979e9d5d8bd03e2b3cb8d0c68623e4d717d0f87b1
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
- # Process the payment state
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.find_by(iso: country_iso) || Spree::Country.default
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
- case event.type
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.status == 'requires_capture'
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
- # New system: PaymentSession
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: 10.seconds).perform_later(payment_intent.id)
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[payment_intent.payment_failed payment_intent.succeeded setup_intent.succeeded]
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
@@ -1,5 +1,5 @@
1
1
  module SpreeStripe
2
- VERSION = '1.6.0'.freeze
2
+ VERSION = '1.7.0'.freeze
3
3
 
4
4
  def gem_version
5
5
  Gem::Version.new(VERSION)
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.6.0
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.6.0
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.6.0
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