spree_stripe 1.7.0 → 1.8.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree_stripe/confirm_payments_controller.rb +52 -0
  3. data/app/helpers/spree_stripe/base_helper.rb +24 -2
  4. data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +256 -338
  5. data/app/javascript/spree_stripe/controllers/stripe_controller.js +129 -206
  6. data/app/models/spree/payment_sessions/stripe.rb +1 -2
  7. data/app/models/spree_stripe/gateway/payment_intents.rb +0 -4
  8. data/app/models/spree_stripe/gateway/payment_sessions.rb +2 -2
  9. data/app/models/spree_stripe/order_decorator.rb +0 -27
  10. data/app/models/spree_stripe/shipment_decorator.rb +3 -3
  11. data/app/services/spree_stripe/create_payment_session.rb +18 -0
  12. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +4 -15
  13. data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +1 -7
  14. data/app/views/spree/checkout/payment/_spree_stripe.html.erb +58 -58
  15. data/app/views/spree_stripe/_quick_checkout.html.erb +9 -15
  16. data/config/locales/en.yml +3 -8
  17. data/config/routes.rb +3 -22
  18. data/db/migrate/20250310152812_setup_spree_stripe_models.rb +0 -17
  19. data/lib/spree_stripe/configuration.rb +0 -1
  20. data/lib/spree_stripe/engine.rb +0 -7
  21. data/lib/spree_stripe/version.rb +1 -1
  22. metadata +5 -44
  23. data/app/controllers/spree_stripe/payment_intents_controller.rb +0 -61
  24. data/app/jobs/spree_stripe/complete_order_job.rb +0 -8
  25. data/app/models/spree_stripe/payment_intent.rb +0 -81
  26. data/app/services/spree_stripe/create_payment_intent.rb +0 -40
  27. data/app/services/spree_stripe/create_setup_intent.rb +0 -20
  28. data/lib/spree_api_v2/spree/api/v2/platform/affirm_serializer.rb +0 -9
  29. data/lib/spree_api_v2/spree/api/v2/platform/after_pay_serializer.rb +0 -9
  30. data/lib/spree_api_v2/spree/api/v2/platform/alipay_serializer.rb +0 -9
  31. data/lib/spree_api_v2/spree/api/v2/platform/ideal_serializer.rb +0 -9
  32. data/lib/spree_api_v2/spree/api/v2/platform/klarna_serializer.rb +0 -9
  33. data/lib/spree_api_v2/spree/api/v2/platform/link_serializer.rb +0 -9
  34. data/lib/spree_api_v2/spree/api/v2/platform/przelewy24_serializer.rb +0 -9
  35. data/lib/spree_api_v2/spree/api/v2/platform/sepa_debit_serializer.rb +0 -9
  36. data/lib/spree_api_v2/spree/api/v2/storefront/stripe/base_controller.rb +0 -21
  37. data/lib/spree_api_v2/spree/api/v2/storefront/stripe/payment_intents_controller.rb +0 -86
  38. data/lib/spree_api_v2/spree/api/v2/storefront/stripe/setup_intents_controller.rb +0 -19
  39. data/lib/spree_api_v2/spree/v2/storefront/affirm_serializer.rb +0 -7
  40. data/lib/spree_api_v2/spree/v2/storefront/after_pay_serializer.rb +0 -7
  41. data/lib/spree_api_v2/spree/v2/storefront/alipay_serializer.rb +0 -7
  42. data/lib/spree_api_v2/spree/v2/storefront/ideal_serializer.rb +0 -7
  43. data/lib/spree_api_v2/spree/v2/storefront/klarna_serializer.rb +0 -7
  44. data/lib/spree_api_v2/spree/v2/storefront/link_serializer.rb +0 -7
  45. data/lib/spree_api_v2/spree/v2/storefront/payment_intent_serializer.rb +0 -15
  46. data/lib/spree_api_v2/spree/v2/storefront/przelewy24_serializer.rb +0 -7
  47. data/lib/spree_api_v2/spree/v2/storefront/sepa_debit_serializer.rb +0 -7
  48. data/lib/spree_api_v2/spree_stripe/v2/storefront/payment_method_serializer_decorator.rb +0 -17
  49. data/lib/spree_stripe/testing_support/factories/payment_intent_factory.rb +0 -24
@@ -1,109 +1,71 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import showFlashMessage from 'spree/storefront/helpers/show_flash_message'
3
3
 
4
+ // Regular Stripe checkout on the v3 Store API — renders the payment form and
5
+ // confirms the payment. Completion is NOT handled here: both new and saved
6
+ // cards redirect to `return_url` (new cards via Stripe's redirect, saved cards
7
+ // manually after inline confirmation), where the server-side
8
+ // ConfirmPaymentsController finalizes the session + cart.
4
9
  export default class extends Controller {
5
10
  static values = {
6
11
  apiKey: String,
7
- paymentIntent: Object,
8
- returnUrl: String,
12
+ clientSecret: String,
13
+ cartId: String,
14
+ cartToken: String,
15
+ spreeApiKey: String,
9
16
  orderEmail: String,
10
- orderToken: String,
17
+ returnUrl: String,
11
18
  colorPrimary: String,
12
- colorBackground: String,
13
19
  colorText: String,
14
- paymentIntentPath: String,
15
- checkoutPath: String,
16
- checkoutValidateOrderForPaymentPath: String,
17
20
  paymentElementAdditionalOptions: { type: Object, default: {} }
18
21
  }
19
22
 
20
- static targets = [
21
- 'paymentElement',
22
- 'loading',
23
- 'spinner',
24
- 'buttonText',
25
- 'defaultCard'
26
- ]
23
+ static targets = ['paymentElement', 'loading', 'defaultCard']
27
24
 
28
25
  connect() {
29
- const stripeOptions = {}
30
-
31
26
  this.submitTarget = document.querySelector('#checkout-payment-submit')
32
27
  this.billingAddressCheckbox = document.querySelector('#order_use_shipping')
33
28
  this.billingAddressForm = document.querySelector('form.edit_order')
34
- this.stripe = Stripe(this.apiKeyValue, stripeOptions)
29
+ this.stripe = Stripe(this.apiKeyValue)
35
30
 
36
31
  if (this.hasDefaultCardTarget) {
37
- this.initializeStripe({
38
- target: { value: this.defaultCardTarget.value }
39
- })
32
+ this.initializeStripe({ target: { value: this.defaultCardTarget.value } })
40
33
  } else {
41
34
  this.initializeStripe({ target: { value: 'on' } })
42
35
  }
43
36
 
44
- // hijack the submit button to call the submit method
45
- this.submitTarget.addEventListener('click', this.submit.bind(this))
37
+ if (this.submitTarget) {
38
+ this.submitTarget.addEventListener('click', this.submit.bind(this))
39
+ }
46
40
  }
47
41
 
48
- // Fetches a payment intent
49
- async initializeStripe(e) {
42
+ // Mounts the Payment Element for a new card, or records the selected saved card.
43
+ initializeStripe(e) {
50
44
  this.setLoading(true)
51
45
 
52
- if (e.target.value == 'on') {
53
- this.stripePaymentMethodId = null
54
- } else {
55
- this.stripePaymentMethodId = e.target.value
56
- }
57
-
58
- const response = await fetch(
59
- this.paymentIntentPathValue,
60
- {
61
- method: 'PATCH',
62
- headers: this.spreeApiHeaders,
63
- body: JSON.stringify({
64
- payment_intent: {
65
- stripe_payment_method_id: this.stripePaymentMethodId
66
- }
67
- })
68
- }
69
- )
70
- const { data, error } = await response.json()
71
-
72
- if (error) {
73
- this.handleError(error)
74
- return
75
- }
46
+ const value = e.target.value
47
+ this.stripePaymentMethodId = value && value !== 'on' ? value : null
76
48
 
77
- if (this.stripePaymentMethodId === null) {
49
+ if (this.stripePaymentMethodId === null && !this.elements) {
78
50
  const appearance = {
79
51
  theme: 'stripe',
80
52
  variables: {
81
53
  colorPrimary: this.colorPrimaryValue,
82
- colorBackground: this.colorBackgroundValue,
83
54
  colorText: this.colorTextValue
84
55
  }
85
56
  }
86
- this.elements = this.stripe.elements({
87
- appearance,
88
- clientSecret: this.paymentIntentValue.client_secret
89
- })
57
+ this.elements = this.stripe.elements({ appearance, clientSecret: this.clientSecretValue })
90
58
  const paymentElement = this.elements.create('payment', {
91
59
  fields: {
92
60
  billingDetails: {
93
61
  name: 'never',
94
62
  email: 'never',
95
- address: {
96
- country: 'never',
97
- postalCode: 'never'
98
- }
63
+ address: { country: 'never', postalCode: 'never' }
99
64
  }
100
65
  },
101
66
  ...this.paymentElementAdditionalOptionsValue
102
67
  })
103
68
  paymentElement.mount(this.paymentElementTarget)
104
- paymentElement.on('change', (event) => {
105
- this.selectedMethod = event.value?.type
106
- })
107
69
  }
108
70
 
109
71
  this.setLoading(false)
@@ -113,202 +75,163 @@ export default class extends Controller {
113
75
  e.preventDefault()
114
76
  this.setLoading(true)
115
77
 
116
- const validated = await this.validateOrderForPayment()
117
-
118
- if (!validated) {
119
- this.setLoading(false)
120
- e.stopImmediatePropagation()
121
- return
122
- }
123
-
124
- const billingAddress = await this.updateBillingAddress()
125
-
126
- if (!billingAddress) {
78
+ const billing = await this.updateBillingAddress()
79
+ if (!billing) {
127
80
  this.setLoading(false)
128
81
  e.stopImmediatePropagation()
129
82
  return
130
83
  }
131
84
 
132
85
  if (this.stripePaymentMethodId) {
133
- const { paymentIntent, error } = await this.stripe.confirmCardPayment(
134
- this.paymentIntentValue.client_secret
135
- )
86
+ // Saved card: confirm inline (Stripe binds the payment method to the
87
+ // intent), then hand off to the return controller — mirrors how Stripe
88
+ // redirects new cards back. The server reads the bound method from the
89
+ // confirmed intent, so no extra session call is needed.
90
+ const { error } = await this.stripe.confirmCardPayment(this.clientSecretValue, {
91
+ payment_method: this.stripePaymentMethodId
92
+ })
136
93
 
137
- this.handleError(error)
94
+ if (error) {
95
+ this.handleError(error)
96
+ this.setLoading(false)
97
+ return
98
+ }
138
99
 
139
100
  window.location.href = this.returnUrlValue
140
101
  } else {
102
+ // New card: confirm with the Payment Element; Stripe redirects to return_url.
141
103
  const elements = this.elements
142
-
143
104
  if (!elements || !elements.getElement('payment')) {
144
- showFlashMessage(
145
- 'An unexpected error occurred. Please refresh the page and try again.',
146
- 'error'
147
- )
105
+ showFlashMessage('An unexpected error occurred. Please refresh the page and try again.', 'error')
106
+ this.setLoading(false)
148
107
  return
149
108
  }
150
109
 
151
110
  const { error } = await this.stripe.confirmPayment({
152
111
  elements,
153
112
  confirmParams: {
154
- payment_method_data: {
155
- billing_details: {
156
- email: this.orderEmailValue,
157
- name: billingAddress.firstname + ' ' + billingAddress.lastname,
158
- address: {
159
- city: billingAddress.city,
160
- country: billingAddress.country_iso,
161
- line1: billingAddress.address1,
162
- line2: billingAddress.address2,
163
- postal_code: billingAddress.zipcode,
164
- state: billingAddress.state_code
165
- }
166
- }
167
- },
168
- return_url: this.returnUrlValue
113
+ return_url: this.returnUrlValue,
114
+ payment_method_data: { billing_details: this.billingDetails(billing) }
169
115
  }
170
116
  })
171
117
 
172
- // This point will only be reached if there is an immediate error when
173
- // confirming the payment. Otherwise, your customer will be redirected to
174
- // your `return_url`. For some payment methods like iDEAL, your customer will
175
- // be redirected to an intermediate site first to authorize the payment, then
176
- // redirected to the `return_url`.
177
- // TODO: we need to remove the payment intent id from this order and create new
178
- this.handleError(error)
179
- }
180
-
181
- this.setLoading(false)
182
- }
183
-
184
- async validateOrderForPayment() {
185
- const response = await fetch(
186
- this.checkoutValidateOrderForPaymentPathValue,
187
- {
188
- method: 'POST',
189
- headers: this.spreeApiHeaders
190
- }
191
- )
192
-
193
- const json = await response.json()
194
-
195
- if (json.meta?.messages?.length) {
196
- const message = [
197
- json.meta.messages,
198
- 'please refresh the page and try again.'
199
- ].join(', ')
200
-
201
- showFlashMessage(message, 'error')
202
- return false
203
- } else {
204
- return true
118
+ // Reached only on an immediate error; otherwise Stripe redirects to return_url.
119
+ if (error) this.handleError(error)
120
+ this.setLoading(false)
205
121
  }
206
122
  }
207
123
 
124
+ // Persists the billing address to the cart via the v3 Store API.
125
+ // Returns the cart's billing_address on success, false on failure.
208
126
  async updateBillingAddress() {
209
- // billing address same as shipping address
127
+ let body
210
128
  if (this.billingAddressCheckbox?.checked) {
211
- const response = await fetch(this.checkoutPathValue, {
212
- method: 'PATCH',
213
- headers: this.spreeApiHeaders,
214
- body: JSON.stringify({
215
- include: 'billing_address',
216
- order: {
217
- use_shipping: true
218
- }
219
- })
220
- });
129
+ body = { use_shipping: true }
130
+ } else if (this.billingAddressForm?.checkValidity()) {
131
+ body = { billing_address: this.readBillingAddressForm() }
132
+ } else {
133
+ if (this.submitTarget) this.submitTarget.disabled = false
134
+ return false
135
+ }
221
136
 
222
- const responseJson = await response.json();
137
+ const response = await fetch(this.cartApiBase, {
138
+ method: 'PATCH',
139
+ headers: this.spreeApiHeaders,
140
+ body: JSON.stringify(body)
141
+ })
142
+ const json = await response.json().catch(() => ({}))
223
143
 
224
- if (response.ok) {
225
- return responseJson.included.find(item => item.type === 'address').attributes;
226
- } else {
227
- const errors = Array.isArray(responseJson.error) ? responseJson.error.join('. ') : responseJson.error || 'Billing address is invalid';
228
- showFlashMessage(errors, 'error');
229
- this.submitTarget.disabled = false;
230
- return false;
231
- }
144
+ if (response.ok) {
145
+ return json.billing_address || {}
232
146
  }
233
147
 
234
- // billing address different from shipping address
235
- if (this.billingAddressForm.checkValidity()) {
236
- const formData = new FormData(this.billingAddressForm);
148
+ showFlashMessage(this.extractErrors(json) || 'Billing address is invalid', 'error')
149
+ if (this.submitTarget) this.submitTarget.disabled = false
150
+ return false
151
+ }
237
152
 
238
- const response = await fetch(this.checkoutPathValue, {
239
- method: 'PATCH',
240
- headers: this.spreeApiHeaders,
241
- body: JSON.stringify({
242
- include: 'billing_address',
243
- order: {
244
- bill_address_attributes: {
245
- firstname: formData.get("order[bill_address_attributes][firstname]"),
246
- lastname: formData.get("order[bill_address_attributes][lastname]"),
247
- address1: formData.get("order[bill_address_attributes][address1]"),
248
- address2: formData.get("order[bill_address_attributes][address2]"),
249
- city: formData.get("order[bill_address_attributes][city]"),
250
- country_id: formData.get("order[bill_address_attributes][country_id]"),
251
- state_id: formData.get("order[bill_address_attributes][state_id]"),
252
- state_name: formData.get("order[bill_address_attributes][state_name]"),
253
- zipcode: formData.get("order[bill_address_attributes][zipcode]"),
254
- phone: formData.get("order[bill_address_attributes][phone]")
255
- }
256
- }
257
- })
258
- });
153
+ readBillingAddressForm() {
154
+ const form = this.billingAddressForm
155
+ const field = (name) => form.querySelector(`[name="order[bill_address_attributes][${name}]"]`)
156
+ const value = (name) => field(name)?.value || null
259
157
 
260
- const responseJson = await response.json();
158
+ const countryOption = field('country_id')?.selectedOptions?.[0]
159
+ const stateOption = field('state_id')?.selectedOptions?.[0]
261
160
 
262
- if (response.ok) {
263
- return responseJson.included.find(item => item.type === 'address').attributes;
264
- } else {
265
- const errors = Array.isArray(responseJson.error) ? responseJson.error.join('. ') : responseJson.error || 'Billing address is invalid';
266
- showFlashMessage(errors, 'error');
161
+ return {
162
+ first_name: value('firstname'),
163
+ last_name: value('lastname'),
164
+ address1: value('address1'),
165
+ address2: value('address2'),
166
+ city: value('city'),
167
+ postal_code: value('zipcode'),
168
+ phone: value('phone'),
169
+ country_iso: countryOption?.dataset?.iso || null,
170
+ state_name: stateOption?.text?.trim() || value('state_name')
171
+ }
172
+ }
267
173
 
268
- this.submitTarget.disabled = false;
269
- return false;
174
+ billingDetails(address) {
175
+ return {
176
+ name: [address.first_name, address.last_name].filter(Boolean).join(' '),
177
+ email: this.orderEmailValue,
178
+ address: {
179
+ city: address.city,
180
+ country: address.country_iso,
181
+ line1: address.address1,
182
+ line2: address.address2,
183
+ postal_code: address.postal_code,
184
+ state: address.state_abbr || address.state_name
270
185
  }
271
- } else {
272
- this.submitTarget.disabled = false;
273
- return false
274
186
  }
275
187
  }
276
188
 
277
189
  handleError(error) {
278
- if (error) {
279
- if (error.type === 'card_error' || error.type === 'validation_error') {
280
- showFlashMessage(error.message, 'error')
281
- } else {
282
- showFlashMessage(
283
- 'An unexpected error occured. Please refresh the page and try again.',
284
- 'error'
285
- )
286
- }
287
-
288
- this.submitTarget.disabled = false;
190
+ if (!error) return
289
191
 
290
- window.scrollTo({
291
- top: 0,
292
- behavior: 'smooth'
293
- })
192
+ // Per Stripe: only card_error / validation_error messages are meant to be
193
+ // shown to customers; other types are developer-facing, so fall back to a
194
+ // generic message.
195
+ if (error.type === 'card_error' || error.type === 'validation_error') {
196
+ showFlashMessage(error.message, 'error')
197
+ } else {
198
+ showFlashMessage('An unexpected error occurred. Please refresh the page and try again.', 'error')
294
199
  }
200
+
201
+ if (this.submitTarget) this.submitTarget.disabled = false
202
+ window.scrollTo({ top: 0, behavior: 'smooth' })
295
203
  }
296
204
 
297
- // Show a spinner on payment submission
298
205
  setLoading(isLoading) {
299
206
  if (isLoading) {
300
- // Disable the button and show a spinner
301
- this.submitTarget.disabled = true
207
+ if (this.submitTarget) this.submitTarget.disabled = true
302
208
  } else {
303
- this.loadingTarget.classList.add('hidden')
304
- this.submitTarget.disabled = false
305
- this.submitTarget.classList.remove('hidden')
209
+ if (this.hasLoadingTarget) this.loadingTarget.classList.add('hidden')
210
+ if (this.submitTarget) {
211
+ this.submitTarget.disabled = false
212
+ this.submitTarget.classList.remove('hidden')
213
+ }
306
214
  }
307
215
  }
308
216
 
217
+ extractErrors(json) {
218
+ if (!json) return null
219
+ if (typeof json.error === 'string') return json.error
220
+ if (Array.isArray(json.error)) return json.error.join('. ')
221
+ if (json.errors) {
222
+ return Array.isArray(json.errors) ? json.errors.join('. ') : Object.values(json.errors).flat().join('. ')
223
+ }
224
+ return null
225
+ }
226
+
227
+ get cartApiBase() {
228
+ return `/api/v3/store/carts/${this.cartIdValue}`
229
+ }
230
+
309
231
  get spreeApiHeaders() {
310
232
  return {
311
- 'X-Spree-Order-Token': this.orderTokenValue,
233
+ 'X-Spree-API-Key': this.spreeApiKeyValue,
234
+ 'X-Spree-Token': this.cartTokenValue,
312
235
  'Content-Type': 'application/json'
313
236
  }
314
237
  }
@@ -2,8 +2,7 @@ module Spree
2
2
  class PaymentSessions::Stripe < PaymentSession
3
3
  delegate :api_options, to: :payment_method
4
4
 
5
- # Duck-type interface matching SpreeStripe::PaymentIntent
6
- # This allows reuse of CompleteOrder and CreatePayment services
5
+ # Duck-type interface consumed by the CompleteOrder and CreatePayment services
7
6
  def stripe_id
8
7
  external_id
9
8
  end
@@ -7,10 +7,6 @@ module SpreeStripe
7
7
  BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
8
8
  MANUAL_CAPTURE_METHOD = 'manual'.freeze
9
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
10
  def payment_intent_accepted?(payment_intent)
15
11
  payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
16
12
  end
@@ -4,7 +4,7 @@ module SpreeStripe
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def session_required?
7
- !SpreeStripe::Config[:use_legacy_payment_intents]
7
+ true
8
8
  end
9
9
 
10
10
  def payment_session_class
@@ -15,7 +15,7 @@ module SpreeStripe
15
15
  total = amount.presence || order.total_minus_store_credits
16
16
  amount_in_cents = Spree::Money.new(total, currency: order.currency).cents
17
17
 
18
- return nil if amount_in_cents.zero?
18
+ raise Spree::Core::GatewayError, I18n.t('spree.stripe.payment_session_errors.zero_amount') if amount_in_cents.zero?
19
19
 
20
20
  customer = fetch_or_create_customer(order: order)
21
21
  stripe_pm_id = external_data[:stripe_payment_method_id] || external_data['stripe_payment_method_id']
@@ -1,36 +1,9 @@
1
1
  module SpreeStripe
2
2
  module OrderDecorator
3
3
  def self.prepended(base)
4
- base.has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', dependent: :destroy
5
-
6
4
  base.store_accessor :private_metadata, :stripe_tax_calculation_id
7
5
  end
8
-
9
- def update_payment_intents
10
- return if completed?
11
- return unless total_minus_store_credits.positive?
12
-
13
- payment_intents.each do |payment_intent|
14
- payment_intent.update!(amount: total_minus_store_credits)
15
- end
16
- end
17
-
18
- def associate_user!(user, override_email = true)
19
- super(user, override_email)
20
-
21
- return if payment_intents.empty?
22
-
23
- responses = payment_intents.map { |payment_intent| payment_intent.update_stripe_payment_intent }
24
- customer_id = responses.find { |response| response.params['customer'].present? }&.params['customer']
25
-
26
- payment_intents.update_all(customer_id: customer_id) if customer_id.present?
27
- end
28
-
29
- def stripe_payment_intent
30
- @stripe_payment_intent ||= payment_intents.last.stripe_payment_intent
31
- end
32
6
  end
33
7
  end
34
8
 
35
9
  Spree::Order.prepend(SpreeStripe::OrderDecorator)
36
- Spree::Order.register_update_hook :update_payment_intents
@@ -10,10 +10,10 @@ module SpreeStripe
10
10
  return unless order.fully_shipped?
11
11
  return if order.stripe_tax_calculation_id.blank?
12
12
 
13
- payment_intent = order.payment_intents.last
14
- return if payment_intent.blank?
13
+ payment_session = order.payment_sessions.where(type: Spree::PaymentSessions::Stripe.name).last
14
+ return if payment_session.blank?
15
15
 
16
- SpreeStripe::CreateTaxTransactionJob.perform_later(order.store_id, payment_intent.stripe_id, order.stripe_tax_calculation_id)
16
+ SpreeStripe::CreateTaxTransactionJob.perform_later(order.store_id, payment_session.stripe_id, order.stripe_tax_calculation_id)
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,18 @@
1
+ module SpreeStripe
2
+ # Reuses the order's pending Stripe payment session (keeping its amount in sync
3
+ # with the order total) or creates a new one via the gateway. The v3 successor
4
+ # to the legacy CreatePaymentIntent service.
5
+ class CreatePaymentSession
6
+ def call(order, gateway)
7
+ amount = order.total_minus_store_credits
8
+ return if Spree::Money.new(amount, currency: order.currency).cents.zero?
9
+
10
+ session = order.payment_sessions.where(payment_method: gateway, status: 'pending').order(:created_at).last
11
+
12
+ return gateway.create_payment_session(order: order) unless session
13
+
14
+ gateway.update_payment_session(payment_session: session, amount: amount) if session.amount != amount
15
+ session
16
+ end
17
+ end
18
+ end
@@ -4,23 +4,12 @@ module SpreeStripe
4
4
  def call(event)
5
5
  stripe_id = event.data.object.id
6
6
 
7
- # New system: PaymentSession
8
7
  payment_session = Spree::PaymentSessions::Stripe.find_by(external_id: stripe_id)
9
- if payment_session.present?
10
- payment_session.fail if payment_session.can_fail?
11
- order = payment_session.order
12
- order.cancel! if !order.canceled? && order.can_cancel?
13
- return
14
- end
8
+ return if payment_session.blank?
15
9
 
16
- # Legacy system: PaymentIntent
17
- payment_intent = SpreeStripe::PaymentIntent.find_by(stripe_id: stripe_id)
18
- return if payment_intent.nil?
19
-
20
- order = payment_intent.order
21
- return if order.canceled?
22
-
23
- order.cancel! if order.can_cancel?
10
+ payment_session.fail if payment_session.can_fail?
11
+ order = payment_session.order
12
+ order.cancel! if !order.canceled? && order.can_cancel?
24
13
  end
25
14
  end
26
15
  end
@@ -4,13 +4,7 @@ module SpreeStripe
4
4
  def call(event)
5
5
  stripe_id = event.data.object[:id]
6
6
 
7
- return if enqueue_complete_order_from_session(stripe_id)
8
-
9
- # Legacy system: PaymentIntent
10
- payment_intent = SpreeStripe::PaymentIntent.find_by(stripe_id: stripe_id)
11
- return if payment_intent.nil?
12
-
13
- SpreeStripe::CompleteOrderJob.set(wait: ENQUEUE_DELAY).perform_later(payment_intent.id)
7
+ enqueue_complete_order_from_session(stripe_id)
14
8
  end
15
9
  end
16
10
  end