spree_stripe 1.5.1 → 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -0
  3. data/app/jobs/spree_stripe/complete_order_from_session_job.rb +12 -0
  4. data/app/models/spree/payment_sessions/stripe.rb +54 -0
  5. data/app/models/spree/payment_setup_sessions/stripe.rb +21 -0
  6. data/app/models/spree_stripe/custom_domain_decorator.rb +3 -1
  7. data/app/models/spree_stripe/gateway/payment_intents.rb +153 -0
  8. data/app/models/spree_stripe/gateway/payment_sessions.rb +180 -0
  9. data/app/models/spree_stripe/gateway/payment_setup_sessions.rb +63 -0
  10. data/app/models/spree_stripe/gateway.rb +55 -125
  11. data/app/presenters/spree_stripe/payment_intent_presenter.rb +8 -2
  12. data/app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb +1 -1
  13. data/app/services/spree_stripe/create_payment_intent.rb +1 -2
  14. data/app/services/spree_stripe/create_source.rb +1 -1
  15. data/app/services/spree_stripe/register_domain.rb +1 -1
  16. data/app/services/spree_stripe/webhook_handlers/base.rb +19 -0
  17. data/app/services/spree_stripe/webhook_handlers/payment_intent_amount_capturable_updated.rb +13 -0
  18. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +13 -1
  19. data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +8 -4
  20. data/config/initializers/stripe.rb +1 -0
  21. data/config/routes.rb +11 -9
  22. data/lib/spree_stripe/configuration.rb +8 -1
  23. data/lib/spree_stripe/engine.rb +13 -2
  24. data/lib/spree_stripe/testing_support/factories/stripe_payment_session_factory.rb +33 -0
  25. data/lib/spree_stripe/version.rb +1 -1
  26. metadata +38 -29
  27. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/affirm_serializer.rb +0 -0
  28. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/after_pay_serializer.rb +0 -0
  29. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/alipay_serializer.rb +0 -0
  30. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/ideal_serializer.rb +0 -0
  31. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/klarna_serializer.rb +0 -0
  32. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/link_serializer.rb +0 -0
  33. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/przelewy24_serializer.rb +0 -0
  34. /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/platform/sepa_debit_serializer.rb +0 -0
  35. /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/base_controller.rb +0 -0
  36. /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/payment_intents_controller.rb +0 -0
  37. /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/stripe/setup_intents_controller.rb +0 -0
  38. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/affirm_serializer.rb +0 -0
  39. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/after_pay_serializer.rb +0 -0
  40. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/alipay_serializer.rb +0 -0
  41. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/ideal_serializer.rb +0 -0
  42. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/klarna_serializer.rb +0 -0
  43. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/link_serializer.rb +0 -0
  44. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/payment_intent_serializer.rb +0 -0
  45. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/przelewy24_serializer.rb +0 -0
  46. /data/{app/serializers → lib/spree_api_v2}/spree/v2/storefront/sepa_debit_serializer.rb +0 -0
  47. /data/{app/serializers → lib/spree_api_v2}/spree_stripe/v2/storefront/payment_method_serializer_decorator.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 296413327864ba7d62dbcbe59c93f4b20bbe8ca0eaed1f0379ab30548da13412
4
- data.tar.gz: 58bb81757cc9097d009fc3d0cb3733455ab0e22c5012a0310ca9e583da7bb744
3
+ metadata.gz: a10e7ebb4ff5cc5960d0ecb894cfc0e15ef4ed058bf2133fc412ca9fc4c95910
4
+ data.tar.gz: 49fd3295806ed476343813ac3ce3356d74054bf84db8ecf9f4ade86099f139c3
5
5
  SHA512:
6
- metadata.gz: 4aca196555667bc988d69382638bd235450deb8a66da6680915476e771c3c745d32f0ce2300ba737863998ec012d8929144cd8792d8c1c121d92fdbf065fef80
7
- data.tar.gz: 5ad609bed31583b62956e3be2517fc7b42a4a99beec2067cc4993ee4f771f38d2cb5040d477154cc0cc6132ea1a85ddddb200279ce404f6ec83de2ca56974b24
6
+ metadata.gz: 3ceff451db2064b42fe695aaa91b0ff7c2512fa455504ba459f84a1c788731fd490d85c3a950972a3a7942acaf9cce64a2d6d2de2ce0ace9d2bfccacccc95770
7
+ data.tar.gz: cd0aff0b871a315fd19b5529912ce431ab5c5c35657bab1853fd2548e80591f031f79ee81bab107f64da05f985bfad7271b288fc813a87d4a583935581ad66c0
data/Rakefile CHANGED
@@ -21,5 +21,7 @@ task :test_app do
21
21
  install_storefront: true,
22
22
  install_admin: true
23
23
  )
24
+ system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_posts:install')
24
25
  system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_api_v2:install')
26
+ system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_product_properties:install')
25
27
  end
@@ -0,0 +1,12 @@
1
+ module SpreeStripe
2
+ class CompleteOrderFromSessionJob < BaseJob
3
+ def perform(payment_session_id)
4
+ payment_session = Spree::PaymentSessions::Stripe.find(payment_session_id)
5
+
6
+ # PaymentSessions::Stripe duck-types as PaymentIntent
7
+ SpreeStripe::CompleteOrder.new(payment_intent: payment_session).call
8
+
9
+ payment_session.complete unless payment_session.completed?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ class PaymentSessions::Stripe < PaymentSession
3
+ delegate :api_options, to: :payment_method
4
+
5
+ # Duck-type interface matching SpreeStripe::PaymentIntent
6
+ # This allows reuse of CompleteOrder and CreatePayment services
7
+ def stripe_id
8
+ external_id
9
+ end
10
+
11
+ def client_secret
12
+ external_data&.dig('client_secret')
13
+ end
14
+
15
+ def ephemeral_key_secret
16
+ external_data&.dig('ephemeral_key_secret')
17
+ end
18
+
19
+ def stripe_payment_intent
20
+ @stripe_payment_intent ||= payment_method.retrieve_payment_intent(external_id)
21
+ end
22
+
23
+ def stripe_charge
24
+ @stripe_charge ||= begin
25
+ latest_charge = stripe_payment_intent.latest_charge
26
+ latest_charge.present? ? payment_method.retrieve_charge(latest_charge) : nil
27
+ end
28
+ end
29
+
30
+ def accepted?
31
+ payment_method.payment_intent_accepted?(stripe_payment_intent)
32
+ end
33
+
34
+ def successful?
35
+ stripe_payment_intent.status == 'succeeded'
36
+ end
37
+
38
+ def charge_not_required?
39
+ payment_method.payment_intent_charge_not_required?(stripe_payment_intent)
40
+ end
41
+
42
+ def find_or_create_payment!(metadata = {})
43
+ return unless persisted?
44
+ return payment if payment.present?
45
+
46
+ SpreeStripe::CreatePayment.new(
47
+ order: order,
48
+ payment_intent: self,
49
+ gateway: payment_method,
50
+ amount: amount
51
+ ).call
52
+ end
53
+ end
54
+ end
@@ -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
@@ -15,4 +15,6 @@ module SpreeStripe
15
15
  end
16
16
  end
17
17
 
18
- Spree::CustomDomain.prepend(SpreeStripe::CustomDomainDecorator)
18
+ if defined?(Spree::CustomDomain)
19
+ Spree::CustomDomain.prepend(SpreeStripe::CustomDomainDecorator)
20
+ end
@@ -0,0 +1,153 @@
1
+ module SpreeStripe
2
+ class Gateway < ::Spree::Gateway
3
+ module PaymentIntents
4
+ extend ActiveSupport::Concern
5
+
6
+ DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = %w[sepa_debit us_bank_account].freeze
7
+ BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
8
+ MANUAL_CAPTURE_METHOD = 'manual'.freeze
9
+
10
+ included do
11
+ has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', foreign_key: 'payment_method_id', dependent: :delete_all
12
+ end
13
+
14
+ def payment_intent_accepted?(payment_intent)
15
+ payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
16
+ end
17
+
18
+ def payment_intent_delayed_notification?(payment_intent)
19
+ payment_method = payment_intent.payment_method
20
+ return false unless payment_method.respond_to?(:type)
21
+
22
+ payment_intent.payment_method.type.in?(DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES)
23
+ end
24
+
25
+ def payment_intent_charge_not_required?(payment_intent)
26
+ payment_intent_bank_payment_method?(payment_intent)
27
+ end
28
+
29
+ def payment_intent_bank_payment_method?(payment_intent)
30
+ payment_method = payment_intent.payment_method
31
+ return false unless payment_method.respond_to?(:type)
32
+
33
+ payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
34
+ end
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
+
44
+ # Creates a Stripe payment intent for the order
45
+ #
46
+ # @param amount_in_cents [Integer] the amount in cents
47
+ # @param order [Spree::Order] the order to create a payment intent for
48
+ # @param payment_method_id [String] Stripe payment method id to use, eg. a card token
49
+ # @param off_session [Boolean] whether the payment intent is off session
50
+ # @param customer_profile_id [String] Stripe customer profile id to use, eg. cus_123
51
+ # @return [Spree::PaymentResponse] the response from the payment intent creation
52
+ def create_payment_intent(amount_in_cents, order, payment_method_id: nil, off_session: false, customer_profile_id: nil)
53
+ payload = SpreeStripe::PaymentIntentPresenter.new(
54
+ amount: amount_in_cents,
55
+ order: order,
56
+ customer: customer_profile_id || fetch_or_create_customer(order: order)&.profile_id,
57
+ payment_method_id: payment_method_id,
58
+ off_session: off_session,
59
+ capture_method: stripe_capture_method
60
+ ).call
61
+
62
+ protect_from_error do
63
+ response = send_request { |opts| Stripe::PaymentIntent.create(payload, opts) }
64
+
65
+ success(response.id, response)
66
+ end
67
+ end
68
+
69
+ # Updates a Stripe payment intent for the order
70
+ #
71
+ # @param payment_intent_id [String] Stripe payment intent id
72
+ # @param amount_in_cents [Integer] the amount in cents
73
+ # @param order [Spree::Order] the order to update the payment intent for
74
+ # @param payment_method_id [String] Stripe payment method id to use, eg. a card token
75
+ # @return [Spree::PaymentResponse] the response from the payment intent update
76
+ def update_payment_intent(payment_intent_id, amount_in_cents, order, payment_method_id = nil)
77
+ protect_from_error do
78
+ payload = SpreeStripe::PaymentIntentPresenter.new(
79
+ amount: amount_in_cents,
80
+ order: order,
81
+ customer: fetch_or_create_customer(order: order)&.profile_id,
82
+ payment_method_id: payment_method_id
83
+ ).call.slice(:amount, :currency, :payment_method, :shipping, :customer)
84
+
85
+ response = send_request { |opts| Stripe::PaymentIntent.update(payment_intent_id, payload, opts) }
86
+
87
+ success(response.id, response)
88
+ end
89
+ end
90
+
91
+ def retrieve_payment_intent(payment_intent_id)
92
+ send_request { |opts| Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }, opts) }
93
+ end
94
+
95
+ def confirm_payment_intent(payment_intent_id)
96
+ send_request { |opts| Stripe::PaymentIntent.confirm(payment_intent_id, {}, opts) }
97
+ end
98
+
99
+ def capture_payment_intent(payment_intent_id, amount_in_cents)
100
+ send_request { |opts| Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }, opts) }
101
+ end
102
+
103
+ # Cancels a Stripe payment intent
104
+ #
105
+ # @param payment_intent_id [String] Stripe payment intent ID, eg. pi_123
106
+ def cancel_payment_intent(payment_intent_id)
107
+ send_request { |opts| Stripe::PaymentIntent.cancel(payment_intent_id, {}, opts) }
108
+ end
109
+
110
+ # Ensures a Stripe payment intent exists for Spree payment
111
+ #
112
+ # @param payment [Spree::Payment] the payment to ensure a payment intent exists for
113
+ # @param amount_in_cents [Integer] the amount in cents
114
+ # @param payment_source [Spree::CreditCard | Spree::PaymentSource] the payment source to use
115
+ # @return [Spree::Payment] the payment with the payment intent
116
+ def ensure_payment_intent_exists_for_payment(payment, amount_in_cents = nil, payment_source = nil)
117
+ return payment if payment.response_code.present?
118
+
119
+ amount_in_cents ||= payment.display_amount.cents
120
+ payment_source ||= payment.source
121
+
122
+ response = create_payment_intent(
123
+ amount_in_cents,
124
+ payment.order,
125
+ payment_method_id: payment_source.gateway_payment_profile_id,
126
+ off_session: true,
127
+ customer_profile_id: payment_source.gateway_customer_profile_id
128
+ )
129
+
130
+ payment.update_columns(
131
+ response_code: response.authorization,
132
+ updated_at: Time.current
133
+ )
134
+
135
+ payment
136
+ end
137
+
138
+ private
139
+
140
+ def stripe_capture_method
141
+ auto_capture? ? nil : MANUAL_CAPTURE_METHOD
142
+ end
143
+
144
+ def payment_intent_accepted_statuses(payment_intent)
145
+ statuses = %w[succeeded]
146
+ statuses << 'requires_capture' if payment_intent_manual_capture?(payment_intent)
147
+ statuses << 'processing' if payment_intent_delayed_notification?(payment_intent)
148
+ statuses << 'requires_action' if payment_intent_charge_not_required?(payment_intent)
149
+ statuses
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,180 @@
1
+ module SpreeStripe
2
+ class Gateway < ::Spree::Gateway
3
+ module PaymentSessions
4
+ extend ActiveSupport::Concern
5
+
6
+ def session_required?
7
+ !SpreeStripe::Config[:use_legacy_payment_intents]
8
+ end
9
+
10
+ def payment_session_class
11
+ Spree::PaymentSessions::Stripe
12
+ end
13
+
14
+ def create_payment_session(order:, amount: nil, external_data: {})
15
+ total = amount.presence || order.total_minus_store_credits
16
+ amount_in_cents = Spree::Money.new(total, currency: order.currency).cents
17
+
18
+ return nil if amount_in_cents.zero?
19
+
20
+ customer = fetch_or_create_customer(order: order)
21
+ stripe_pm_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
22
+
23
+ response = create_payment_intent(
24
+ amount_in_cents, order,
25
+ payment_method_id: stripe_pm_id,
26
+ customer_profile_id: customer&.profile_id
27
+ )
28
+
29
+ ephemeral_key_response = create_ephemeral_key(customer.profile_id) if customer.present?
30
+ ephemeral_key_secret = ephemeral_key_response&.params&.dig('secret')
31
+
32
+ payment_session_class.create!(
33
+ order: order,
34
+ payment_method: self,
35
+ amount: total,
36
+ currency: order.currency,
37
+ status: 'pending',
38
+ external_id: response.authorization,
39
+ customer: order.user,
40
+ customer_external_id: customer&.profile_id,
41
+ external_data: {
42
+ 'client_secret' => response.params['client_secret'],
43
+ 'ephemeral_key_secret' => ephemeral_key_secret,
44
+ 'stripe_payment_method_id' => stripe_pm_id
45
+ }.compact
46
+ )
47
+ end
48
+
49
+ def update_payment_session(payment_session:, amount: nil, external_data: {})
50
+ attrs = {}
51
+ amount_in_cents = nil
52
+
53
+ if amount.present?
54
+ attrs[:amount] = amount
55
+ amount_in_cents = Spree::Money.new(amount, currency: payment_session.currency).cents
56
+ end
57
+
58
+ stripe_pm_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
59
+
60
+ update_payment_intent(
61
+ payment_session.external_id,
62
+ amount_in_cents || payment_session.amount_in_cents,
63
+ payment_session.order,
64
+ stripe_pm_id
65
+ )
66
+
67
+ if external_data.present?
68
+ attrs[:external_data] = (payment_session.external_data || {}).merge(external_data.stringify_keys)
69
+ end
70
+
71
+ payment_session.update!(attrs) if attrs.any?
72
+ end
73
+
74
+ # Completes a payment session by verifying with Stripe, creating the
75
+ # Payment record, and patching wallet address data.
76
+ #
77
+ # Does NOT complete the order — that is handled by Carts::Complete
78
+ # (called by the storefront or by the webhook handler).
79
+ def complete_payment_session(payment_session:, params: {})
80
+ stripe_pi = retrieve_payment_intent(payment_session.external_id)
81
+
82
+ if payment_intent_accepted?(stripe_pi)
83
+ payment_session.process if payment_session.can_process?
84
+
85
+ charge = payment_session.stripe_charge
86
+
87
+ # Patch wallet billing address (Apple Pay, Google Pay)
88
+ patch_wallet_address(payment_session.order, charge) if charge.present?
89
+
90
+ # Create the Payment record
91
+ payment_session.find_or_create_payment!
92
+
93
+ # `else` covers requires_capture (manual capture), processing (delayed-notification
94
+ # banks), and requires_action (bank transfer awaiting funds) — all auth-only states.
95
+ payment = payment_session.payment
96
+ if payment.present? && !payment.completed?
97
+ if payment_intent_successful?(stripe_pi)
98
+ payment.process!
99
+ else
100
+ payment.authorize!
101
+ end
102
+ end
103
+
104
+ payment_session.complete unless payment_session.completed?
105
+ else
106
+ payment_session.fail if payment_session.can_fail?
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def payment_intent_successful?(stripe_pi)
113
+ stripe_pi.status == 'succeeded'
114
+ end
115
+
116
+ # Patches the order's billing address with data from the Stripe charge.
117
+ # Needed for quick checkout (Apple Pay/Google Pay) where the storefront
118
+ # doesn't have the billing address before payment confirmation.
119
+ def patch_wallet_address(order, charge)
120
+ return if charge.blank?
121
+
122
+ billing_details = charge.billing_details
123
+ address = billing_details.address
124
+
125
+ order.email ||= billing_details.email
126
+ order.save! if order.email_changed?
127
+
128
+ # Skip if billing address is already valid
129
+ return if order.bill_address.present? && order.bill_address.valid?
130
+
131
+ country_iso = address.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')
135
+
136
+ order.bill_address ||= Spree::Address.new(country: country, user: order.user)
137
+ order.bill_address.quick_checkout = true
138
+
139
+ first_name = billing_details.name&.split(' ')&.first || order.ship_address&.first_name || order.user&.first_name
140
+ last_name = billing_details.name&.split(' ')&.last || order.ship_address&.last_name || order.user&.last_name
141
+
142
+ order.bill_address.first_name ||= first_name
143
+ order.bill_address.last_name ||= last_name
144
+ order.bill_address.phone ||= billing_details.phone
145
+ order.bill_address.address1 ||= address.line1
146
+ order.bill_address.address2 ||= address.line2
147
+ order.bill_address.city ||= address.city
148
+ order.bill_address.zipcode ||= address.postal_code
149
+
150
+ state_name = address.state
151
+ if country.states_required?
152
+ order.bill_address.state = country.states.find_all_by_name_or_abbr(state_name)&.first
153
+ else
154
+ order.bill_address.state_name = state_name
155
+ end
156
+ order.bill_address.state_name ||= state_name
157
+
158
+ if order.bill_address.invalid?
159
+ return if order.ship_address.blank?
160
+
161
+ order.bill_address = order.ship_address
162
+ end
163
+
164
+ order.bill_address.save! if order.bill_address&.changed?
165
+ order.save!
166
+
167
+ copy_bill_info_to_user(order) if order.user.present?
168
+ end
169
+
170
+ def copy_bill_info_to_user(order)
171
+ user = order.user
172
+ user.first_name ||= order.bill_address.first_name
173
+ user.last_name ||= order.bill_address.last_name
174
+ user.phone ||= order.bill_address.phone
175
+ user.bill_address_id ||= order.bill_address.id
176
+ user.save! if user.changed?
177
+ end
178
+ end
179
+ end
180
+ end
@@ -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