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.
- checksums.yaml +4 -4
- data/app/controllers/spree_stripe/confirm_payments_controller.rb +52 -0
- data/app/helpers/spree_stripe/base_helper.rb +24 -2
- data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +256 -338
- data/app/javascript/spree_stripe/controllers/stripe_controller.js +129 -206
- data/app/models/spree/payment_sessions/stripe.rb +1 -2
- data/app/models/spree_stripe/gateway/payment_intents.rb +0 -4
- data/app/models/spree_stripe/gateway/payment_sessions.rb +2 -2
- data/app/models/spree_stripe/order_decorator.rb +0 -27
- data/app/models/spree_stripe/shipment_decorator.rb +3 -3
- data/app/services/spree_stripe/create_payment_session.rb +18 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +4 -15
- data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +1 -7
- data/app/views/spree/checkout/payment/_spree_stripe.html.erb +58 -58
- data/app/views/spree_stripe/_quick_checkout.html.erb +9 -15
- data/config/locales/en.yml +3 -8
- data/config/routes.rb +3 -22
- data/db/migrate/20250310152812_setup_spree_stripe_models.rb +0 -17
- data/lib/spree_stripe/configuration.rb +0 -1
- data/lib/spree_stripe/engine.rb +0 -7
- data/lib/spree_stripe/version.rb +1 -1
- metadata +5 -44
- data/app/controllers/spree_stripe/payment_intents_controller.rb +0 -61
- data/app/jobs/spree_stripe/complete_order_job.rb +0 -8
- data/app/models/spree_stripe/payment_intent.rb +0 -81
- data/app/services/spree_stripe/create_payment_intent.rb +0 -40
- data/app/services/spree_stripe/create_setup_intent.rb +0 -20
- data/lib/spree_api_v2/spree/api/v2/platform/affirm_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/after_pay_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/alipay_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/ideal_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/klarna_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/link_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/przelewy24_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/platform/sepa_debit_serializer.rb +0 -9
- data/lib/spree_api_v2/spree/api/v2/storefront/stripe/base_controller.rb +0 -21
- data/lib/spree_api_v2/spree/api/v2/storefront/stripe/payment_intents_controller.rb +0 -86
- data/lib/spree_api_v2/spree/api/v2/storefront/stripe/setup_intents_controller.rb +0 -19
- data/lib/spree_api_v2/spree/v2/storefront/affirm_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/after_pay_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/alipay_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/ideal_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/klarna_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/link_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/payment_intent_serializer.rb +0 -15
- data/lib/spree_api_v2/spree/v2/storefront/przelewy24_serializer.rb +0 -7
- data/lib/spree_api_v2/spree/v2/storefront/sepa_debit_serializer.rb +0 -7
- data/lib/spree_api_v2/spree_stripe/v2/storefront/payment_method_serializer_decorator.rb +0 -17
- 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
|
-
|
|
8
|
-
|
|
12
|
+
clientSecret: String,
|
|
13
|
+
cartId: String,
|
|
14
|
+
cartToken: String,
|
|
15
|
+
spreeApiKey: String,
|
|
9
16
|
orderEmail: String,
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
37
|
+
if (this.submitTarget) {
|
|
38
|
+
this.submitTarget.addEventListener('click', this.submit.bind(this))
|
|
39
|
+
}
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
//
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
127
|
+
let body
|
|
210
128
|
if (this.billingAddressCheckbox?.checked) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
235
|
-
if (this.
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
158
|
+
const countryOption = field('country_id')?.selectedOptions?.[0]
|
|
159
|
+
const stateOption = field('state_id')?.selectedOptions?.[0]
|
|
261
160
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
return if
|
|
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,
|
|
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.
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|