spree_stripe 1.0.2

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +14 -0
  3. data/README.md +132 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/spree_stripe_manifest.js +3 -0
  6. data/app/controllers/spree/api/v2/storefront/stripe/base_controller.rb +21 -0
  7. data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +61 -0
  8. data/app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb +19 -0
  9. data/app/controllers/spree_stripe/apple_pay_domain_verification_controller.rb +11 -0
  10. data/app/controllers/spree_stripe/payment_intents_controller.rb +61 -0
  11. data/app/controllers/spree_stripe/store_controller_decorator.rb +9 -0
  12. data/app/controllers/stripe_event/webhook_controller_decorator.rb +17 -0
  13. data/app/helpers/spree_stripe/base_helper.rb +13 -0
  14. data/app/javascript/spree_stripe/application.js +18 -0
  15. data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +452 -0
  16. data/app/javascript/spree_stripe/controllers/stripe_controller.js +311 -0
  17. data/app/jobs/spree_stripe/base_job.rb +5 -0
  18. data/app/jobs/spree_stripe/complete_order_job.rb +8 -0
  19. data/app/jobs/spree_stripe/create_webhook_endpoint_job.rb +7 -0
  20. data/app/jobs/spree_stripe/register_domain_job.rb +20 -0
  21. data/app/models/spree_stripe/base.rb +6 -0
  22. data/app/models/spree_stripe/credit_card_decorator.rb +13 -0
  23. data/app/models/spree_stripe/custom_domain_decorator.rb +18 -0
  24. data/app/models/spree_stripe/gateway.rb +366 -0
  25. data/app/models/spree_stripe/order_decorator.rb +19 -0
  26. data/app/models/spree_stripe/payment_decorator.rb +46 -0
  27. data/app/models/spree_stripe/payment_intent.rb +66 -0
  28. data/app/models/spree_stripe/payment_method_decorator.rb +18 -0
  29. data/app/models/spree_stripe/payment_methods_webhook_key.rb +8 -0
  30. data/app/models/spree_stripe/payment_source_decorator.rb +9 -0
  31. data/app/models/spree_stripe/payment_sources/affirm.rb +9 -0
  32. data/app/models/spree_stripe/payment_sources/after_pay.rb +13 -0
  33. data/app/models/spree_stripe/payment_sources/alipay.rb +9 -0
  34. data/app/models/spree_stripe/payment_sources/ideal.rb +15 -0
  35. data/app/models/spree_stripe/payment_sources/klarna.rb +9 -0
  36. data/app/models/spree_stripe/payment_sources/link.rb +13 -0
  37. data/app/models/spree_stripe/payment_sources/przelewy24.rb +11 -0
  38. data/app/models/spree_stripe/payment_sources/sepa_debit.rb +13 -0
  39. data/app/models/spree_stripe/store_decorator.rb +24 -0
  40. data/app/models/spree_stripe/webhook_key.rb +14 -0
  41. data/app/presenters/spree_stripe/customer_presenter.rb +35 -0
  42. data/app/presenters/spree_stripe/payment_intent_presenter.rb +103 -0
  43. data/app/serializers/spree/api/v2/platform/affirm_serializer.rb +9 -0
  44. data/app/serializers/spree/api/v2/platform/after_pay_serializer.rb +9 -0
  45. data/app/serializers/spree/api/v2/platform/alipay_serializer.rb +9 -0
  46. data/app/serializers/spree/api/v2/platform/ideal_serializer.rb +9 -0
  47. data/app/serializers/spree/api/v2/platform/klarna_serializer.rb +9 -0
  48. data/app/serializers/spree/api/v2/platform/link_serializer.rb +9 -0
  49. data/app/serializers/spree/api/v2/platform/przelewy24_serializer.rb +9 -0
  50. data/app/serializers/spree/api/v2/platform/sepa_debit_serializer.rb +9 -0
  51. data/app/serializers/spree/v2/storefront/affirm_serializer.rb +7 -0
  52. data/app/serializers/spree/v2/storefront/after_pay_serializer.rb +7 -0
  53. data/app/serializers/spree/v2/storefront/alipay_serializer.rb +7 -0
  54. data/app/serializers/spree/v2/storefront/ideal_serializer.rb +7 -0
  55. data/app/serializers/spree/v2/storefront/klarna_serializer.rb +7 -0
  56. data/app/serializers/spree/v2/storefront/link_serializer.rb +7 -0
  57. data/app/serializers/spree/v2/storefront/payment_intent_serializer.rb +11 -0
  58. data/app/serializers/spree/v2/storefront/przelewy24_serializer.rb +7 -0
  59. data/app/serializers/spree/v2/storefront/sepa_debit_serializer.rb +7 -0
  60. data/app/services/spree_stripe/complete_order.rb +99 -0
  61. data/app/services/spree_stripe/create_gateway_webhooks.rb +55 -0
  62. data/app/services/spree_stripe/create_payment.rb +43 -0
  63. data/app/services/spree_stripe/create_payment_intent.rb +40 -0
  64. data/app/services/spree_stripe/create_setup_intent.rb +20 -0
  65. data/app/services/spree_stripe/create_source.rb +87 -0
  66. data/app/services/spree_stripe/register_domain.rb +22 -0
  67. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +18 -0
  68. data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +13 -0
  69. data/app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb +34 -0
  70. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_stripe.html.erb +0 -0
  71. data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_stripe.html.erb +47 -0
  72. data/app/views/spree/admin/payment_methods/descriptions/_spree_stripe.html.erb +15 -0
  73. data/app/views/spree/checkout/payment/_spree_stripe.html.erb +63 -0
  74. data/app/views/spree_stripe/_head.html.erb +2 -0
  75. data/app/views/spree_stripe/_quick_checkout.html.erb +34 -0
  76. data/config/importmap.rb +8 -0
  77. data/config/initializers/spree.rb +8 -0
  78. data/config/initializers/stripe.rb +14 -0
  79. data/config/locales/en.yml +5 -0
  80. data/config/routes.rb +24 -0
  81. data/db/migrate/20250310152812_setup_spree_stripe_models.rb +41 -0
  82. data/lib/generators/spree_stripe/install/install_generator.rb +20 -0
  83. data/lib/spree_stripe/configuration.rb +5 -0
  84. data/lib/spree_stripe/engine.rb +36 -0
  85. data/lib/spree_stripe/factories.rb +3 -0
  86. data/lib/spree_stripe/testing_support/factories/after_pay_payment_source_factory.rb +6 -0
  87. data/lib/spree_stripe/testing_support/factories/alipay_payment_source_factory.rb +6 -0
  88. data/lib/spree_stripe/testing_support/factories/gateway_factory.rb +23 -0
  89. data/lib/spree_stripe/testing_support/factories/ideal_payment_source_factory.rb +6 -0
  90. data/lib/spree_stripe/testing_support/factories/klarna_payment_source_factory.rb +6 -0
  91. data/lib/spree_stripe/testing_support/factories/link_payment_source_factory.rb +6 -0
  92. data/lib/spree_stripe/testing_support/factories/payment_intent_factory.rb +24 -0
  93. data/lib/spree_stripe/testing_support/factories/payment_source_factory.rb +8 -0
  94. data/lib/spree_stripe/testing_support/factories/przelewy24_payment_source_factory.rb +6 -0
  95. data/lib/spree_stripe/testing_support/factories/sepa_debit_payment_source_factory.rb +6 -0
  96. data/lib/spree_stripe/testing_support/factories/webhook_key_factory.rb +23 -0
  97. data/lib/spree_stripe/version.rb +7 -0
  98. data/lib/spree_stripe.rb +14 -0
  99. data/vendor/javascript/@stripe--stripe-js--dist--pure.esm.js.js +4 -0
  100. metadata +295 -0
@@ -0,0 +1,452 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { loadStripe } from '@stripe/stripe-js/pure'
3
+ import showFlashMessage from 'spree/storefront/helpers/show_flash_message'
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ apiKey: String,
8
+ orderToken: String,
9
+ clientSecret: String,
10
+ currency: String,
11
+ amount: Number,
12
+ availableCountries: Array,
13
+ giftCardCode: String,
14
+ giftCardAmount: Number,
15
+ borderRadius: Number,
16
+ height: Number,
17
+ theme: String,
18
+ maxRows: Number,
19
+ maxColumns: Number,
20
+ buttonWidth: Number,
21
+ storeUrl: String,
22
+ returnUrl: String,
23
+ }
24
+
25
+ static targets = ['container']
26
+
27
+ connect() {
28
+ this.paymentMethod = null
29
+ this.initStripe()
30
+ this.shippingRates = []
31
+ this.currentShippingOptionId = null
32
+ }
33
+
34
+ initStripe() {
35
+ if (typeof Stripe === 'undefined') {
36
+ loadStripe(this.apiKeyValue).then((stripe) => {
37
+ this.stripe = stripe
38
+ this.prepareExpressCheckoutElement()
39
+ })
40
+ } else if (typeof this.stripe !== 'function') {
41
+ this.stripe = Stripe(this.apiKeyValue)
42
+ this.prepareExpressCheckoutElement()
43
+ }
44
+ }
45
+
46
+ prepareExpressCheckoutElement() {
47
+ this.elements = this.stripe.elements({
48
+ mode: 'payment',
49
+ currency: this.currencyValue,
50
+ amount: parseInt(this.amountValue),
51
+ appearance: {
52
+ theme: 'stripe',
53
+ variables: {
54
+ borderRadius: this.hasBorderRadiusValue ? `${this.borderRadiusValue}px` : undefined
55
+ }
56
+ },
57
+ paymentMethodCreation: 'manual'
58
+ })
59
+
60
+ const prButton = this.elements.create('expressCheckout', {
61
+ wallets: { applePay: 'always', googlePay: 'always' },
62
+ buttonHeight: this.heightValue > 0 ? this.heightValue : undefined,
63
+ buttonTheme: {
64
+ applePay: this.themeValue.length ? this.themeValue : undefined,
65
+ googlePay: this.themeValue.length ? this.themeValue : undefined
66
+ },
67
+ layout: {
68
+ overflow: this.maxRowsValue > 0 ? 'auto' : 'never',
69
+ maxColumns: this.maxColumnsValue > 0 ? this.maxColumnsValue : undefined,
70
+ maxRows: this.maxRowsValue > 0 ? this.maxRowsValue : undefined
71
+ },
72
+ buttonType: {
73
+ applePay: 'check-out',
74
+ googlePay: 'checkout'
75
+ },
76
+ paymentMethodOrder: ['applePay', 'googlePay', 'link']
77
+ })
78
+ prButton.mount('#payment-request-button')
79
+
80
+ prButton.on('ready', ({ availablePaymentMethods }) => {
81
+ if (!availablePaymentMethods) {
82
+ this.containerTarget.style.display = 'hidden'
83
+ return
84
+ }
85
+ const availableMethodsCount = Object.keys(availablePaymentMethods).filter(
86
+ (key) => availablePaymentMethods[key]
87
+ ).length
88
+ if (this.buttonWidthValue > 0) {
89
+ this.containerTarget.style.setProperty(
90
+ '--desktop-max-width',
91
+ this.buttonWidthValue * availableMethodsCount + 'px'
92
+ )
93
+ this.containerTarget.classList.add('desktop-max-width')
94
+ }
95
+ window.parent?.postMessage({ enabledPaymentMethodsCount: availableMethodsCount }, '*')
96
+ })
97
+ prButton.on('click', (event) => {
98
+ this.paymentMethod = event.expressPaymentType
99
+
100
+ event.resolve({
101
+ emailRequired: true,
102
+ shippingAddressRequired: true,
103
+ allowedShippingCountries: this.availableCountriesValue,
104
+ // If we want to collect shipping address then we need to provide at least one shipping option, it will be updated to the real ones in the `shippingaddresschange` event
105
+ shippingRates: [{ id: 'loading', displayName: 'Loading...', amount: 0 }],
106
+ lineItems: [
107
+ { name: 'Subtotal', amount: 0 },
108
+ { name: 'Shipping', amount: 0 },
109
+ { name: 'Store credit', amount: 0 },
110
+ { name: 'Discount', amount: 0 },
111
+ { name: 'Tax', amount: 0 }
112
+ ]
113
+ })
114
+ })
115
+ prButton.on('shippingaddresschange', this.handleAddressChange.bind(this))
116
+ prButton.on('shippingratechange', this.handleShippingOptionChange.bind(this))
117
+ prButton.on('confirm', this.handleFinalizePayment.bind(this))
118
+ prButton.on('cancel', this.handleCancelPayment.bind(this))
119
+ }
120
+
121
+ async handleAddressChange(ev) {
122
+ // Perform server-side request to fetch shipping options
123
+ // https://stripe.com/docs/js/payment_request/events/on_shipping_address_change#payment_request_on_shipping_address_change-handler-shippingAddress
124
+ const orderUpdatePayload = {
125
+ order: {
126
+ ship_address_attributes: {
127
+ // we need to use quick checkout option to skip first/last/street address validation
128
+ // as at this point we don't receive this information as browsers do not share it with us
129
+ quick_checkout: true,
130
+ firstname: ev.name.split(' ')[0],
131
+ lastname: ev.name.split(' ')[1],
132
+ city: ev.address.city,
133
+ zipcode: ev.address.postal_code,
134
+ country_iso: ev.address.country,
135
+ state_name: ev.address.state
136
+ },
137
+ bill_address_id: 'CLEAR' // we need to clear out the bill address to avoid order being pushed to confirm/complete state
138
+ }
139
+ }
140
+
141
+ // 1st we need to persist the address to the order
142
+ const saveAddressResponse = await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout`, {
143
+ method: 'PATCH',
144
+ headers: {
145
+ 'X-Spree-Order-Token': this.orderTokenValue,
146
+ 'Content-Type': 'application/json'
147
+ },
148
+ body: JSON.stringify(orderUpdatePayload)
149
+ })
150
+
151
+ // 2nd we need to push the order to delivery state to generate shipping rates
152
+ if (saveAddressResponse.status === 200) {
153
+ // In case of any error here we have to allow user try again
154
+ try {
155
+ const response = await fetch(
156
+ `${this.storeUrlValue}/api/v2/storefront/checkout/advance?state=delivery&include=shipments.shipping_rates,line_items.vendor`,
157
+ {
158
+ method: 'PATCH',
159
+ headers: {
160
+ 'X-Spree-Order-Token': this.orderTokenValue,
161
+ 'Content-Type': 'application/json'
162
+ },
163
+ body: JSON.stringify({ quick_checkout: true, shipping_method_id: this.currentShippingOptionId })
164
+ }
165
+ )
166
+ const newOrderResponse = await response.json()
167
+ this.shippingRates = newOrderResponse.included.filter((item) => item.type === 'shipping_rate')
168
+
169
+ if (this.shippingRates.length > 0) {
170
+ this.elements.update({ amount: newOrderResponse.data.attributes.total_minus_store_credits_cents })
171
+ const shippingRates = this.shippingOptions(this.shippingRates)
172
+ // We need to select first shipping rate as default, because Apple Pay sometimes doesn't trigger `shippingratechange` event when the modal is opened
173
+ this.currentShippingOptionId = String(shippingRates[0].id)
174
+
175
+ ev.resolve({
176
+ shippingRates: shippingRates,
177
+ lineItems: this.buildLineItems(newOrderResponse)
178
+ })
179
+ return
180
+ }
181
+ } catch (error) {
182
+ ev.reject()
183
+ return
184
+ }
185
+ }
186
+ ev.reject()
187
+ }
188
+
189
+ async handleShippingOptionChange(ev) {
190
+ const { resolve, shippingRate, reject } = ev
191
+
192
+ if (shippingRate) {
193
+ const shippingRateId = String(shippingRate.id).replace(/_google_pay_\d+/, '')
194
+
195
+ if (shippingRateId === 'standard') return resolve()
196
+ if (shippingRateId === 'loading') return reject()
197
+
198
+ this.currentShippingOptionId = shippingRateId
199
+
200
+ const response = await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout/select_shipping_method`, {
201
+ method: 'PATCH',
202
+ headers: {
203
+ 'X-Spree-Order-Token': this.orderTokenValue,
204
+ 'Content-Type': 'application/json'
205
+ },
206
+ body: JSON.stringify({ shipping_method_id: shippingRateId })
207
+ })
208
+
209
+ if (response.status === 200) {
210
+ const newOrderResponse = await response.json()
211
+
212
+ this.elements.update({ amount: newOrderResponse.data.attributes.total_minus_store_credits_cents })
213
+ resolve({ lineItems: this.buildLineItems(newOrderResponse) })
214
+ } else {
215
+ reject()
216
+ }
217
+ } else {
218
+ reject()
219
+ }
220
+ }
221
+
222
+ async handleFinalizePayment(ev) {
223
+ let shippingRateId = (ev.shippingRate?.id || this.currentShippingOptionId)
224
+ if (shippingRateId) {
225
+ shippingRateId = String(shippingRateId).replace(/_google_pay_\d+/, '')
226
+ }
227
+
228
+ if (!shippingRateId || shippingRateId === 'loading') {
229
+ ev.paymentFailed({ reason: 'invalid_shipping_address' })
230
+ return
231
+ }
232
+
233
+ if (this.giftCardCodeValue && this.giftCardAmountValue) {
234
+ const giftCardValidationResponse = await fetch(
235
+ `${this.storeUrlValue}/api/v2/storefront/checkout/validate_gift_card_data`,
236
+ {
237
+ method: 'POST',
238
+ headers: {
239
+ 'X-Spree-Order-Token': this.orderTokenValue,
240
+ 'Content-Type': 'application/json'
241
+ },
242
+ body: JSON.stringify({
243
+ gift_card_code: this.giftCardCodeValue,
244
+ gift_card_amount: this.giftCardAmountValue
245
+ })
246
+ }
247
+ )
248
+ if (giftCardValidationResponse.status === 422) {
249
+ ev.paymentFailed()
250
+ return
251
+ }
252
+ }
253
+
254
+ const validationResponse = await fetch(
255
+ `${this.storeUrlValue}/api/v2/storefront/checkout/validate_order_for_payment?skip_state=true`,
256
+ {
257
+ method: 'POST',
258
+ headers: {
259
+ 'X-Spree-Order-Token': this.orderTokenValue,
260
+ 'Content-Type': 'application/json'
261
+ }
262
+ }
263
+ )
264
+
265
+ if (validationResponse.status === 422) {
266
+ ev.paymentFailed()
267
+ return
268
+ }
269
+
270
+ // Confirm the PaymentIntent without handling potential next actions (yet).
271
+ const { error: confirmError } = await this.elements.submit()
272
+
273
+ if (confirmError) {
274
+ if (confirmError.length > 0) {
275
+ showFlashMessage(confirmError, 'error')
276
+ }
277
+ return
278
+ }
279
+
280
+ // we need to persist some information about the customer to move the order to the next state
281
+ const orderUpdatePayload = {
282
+ order: {
283
+ email: ev.billingDetails.email,
284
+ ship_address_attributes: {
285
+ quick_checkout: true,
286
+ firstname: ev.shippingAddress.name.split(' ')[0],
287
+ lastname: ev.shippingAddress.name.split(' ')[1],
288
+ address1: ev.shippingAddress.address.line1,
289
+ address2: ev.shippingAddress.address.line2,
290
+ city: ev.shippingAddress.address.city,
291
+ zipcode: ev.shippingAddress.address.postal_code,
292
+ country_iso: ev.shippingAddress.address.country,
293
+ state_name: ev.shippingAddress.address.state,
294
+ phone: ev.billingDetails.phone
295
+ }
296
+ },
297
+ do_not_change_state: true
298
+ }
299
+
300
+ const updateResponse = await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout`, {
301
+ method: 'PATCH',
302
+ headers: {
303
+ 'X-Spree-Order-Token': this.orderTokenValue,
304
+ 'Content-Type': 'application/json'
305
+ },
306
+ body: JSON.stringify(orderUpdatePayload)
307
+ })
308
+
309
+ if (updateResponse.status === 200) {
310
+ const advanceResponse = await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout/advance?state=payment`, {
311
+ method: 'PATCH',
312
+ headers: {
313
+ 'X-Spree-Order-Token': this.orderTokenValue,
314
+ 'Content-Type': 'application/json'
315
+ },
316
+ body: JSON.stringify({ shipping_method_id: shippingRateId })
317
+ })
318
+
319
+ if (advanceResponse.status === 200) {
320
+ try {
321
+ const { error: paymentMethodError, paymentMethod } = await this.stripe.createPaymentMethod({
322
+ elements: this.elements
323
+ })
324
+
325
+ if (paymentMethodError) {
326
+ showFlashMessage(error, 'error')
327
+ return
328
+ }
329
+
330
+ const { error } = await this.stripe.confirmPayment({
331
+ clientSecret: this.clientSecretValue,
332
+ confirmParams: {
333
+ payment_method: paymentMethod.id,
334
+ // Stripe will automatically add `payment_intent` and `payment_intent_client_secret` params
335
+ return_url: this.returnUrlValue
336
+ }
337
+ })
338
+ if (error) {
339
+ if (error.length > 0) {
340
+ showFlashMessage(error, 'error')
341
+ }
342
+ return
343
+ }
344
+ } catch (e) {
345
+ console.log(e)
346
+ }
347
+ } else {
348
+ ev.paymentFailed()
349
+ }
350
+ } else {
351
+ ev.paymentFailed()
352
+ }
353
+ }
354
+
355
+ async handleCancelPayment(ev) {
356
+ const orderUpdatePayload = {
357
+ order: {
358
+ ship_address_id: 'CLEAR',
359
+ bill_address_id: 'CLEAR'
360
+ }
361
+ }
362
+
363
+ // reset shipping method for some cases when user select paid shipping method then reload page
364
+ // do not reset shipping method if we have only one or is multivendor order
365
+ let defaultShippingMethodId = this.shippingRates[0]?.attributes?.shipping_method_id
366
+ if (defaultShippingMethodId) {
367
+ defaultShippingMethodId = String(defaultShippingMethodId)
368
+ }
369
+
370
+ if (this.shippingRates?.length > 1 && this.currentShippingOptionId !== defaultShippingMethodId) {
371
+ // reset shipping choice
372
+ await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout/select_shipping_method`, {
373
+ method: 'PATCH',
374
+ headers: {
375
+ 'X-Spree-Order-Token': this.orderTokenValue,
376
+ 'Content-Type': 'application/json'
377
+ },
378
+ body: JSON.stringify({ shipping_method_id: defaultShippingMethodId })
379
+ })
380
+ }
381
+
382
+ // reset addresses
383
+ await fetch(`${this.storeUrlValue}/api/v2/storefront/checkout`, {
384
+ method: 'PATCH',
385
+ headers: {
386
+ 'X-Spree-Order-Token': this.orderTokenValue,
387
+ 'Content-Type': 'application/json'
388
+ },
389
+ body: JSON.stringify(orderUpdatePayload)
390
+ })
391
+
392
+ this.currentShippingOptionId = null
393
+ }
394
+
395
+ shippingOptions(shippingRates) {
396
+ return shippingRates.map((rate) => {
397
+ let id = String(rate.attributes.shipping_method_id)
398
+ if (this.paymentMethod === 'google_pay') {
399
+ // We need to add some random data to the shipping rate to avoid weird issue with Google Pay, in which it clears the shipping rates when a new address is added
400
+ id += `_google_pay_${Math.floor(Math.random() * 100)}`
401
+ }
402
+ return {
403
+ id: id, // shipping rates can be refreshed and removed, shipping methods are more reliable
404
+ displayName: rate.attributes.name,
405
+ deliveryEstimate: rate.attributes.display_delivery_range || '',
406
+ amount: parseInt(rate.attributes.final_price_cents)
407
+ }
408
+ })
409
+ }
410
+
411
+ multiVendorOrder(newOrderResponse) {
412
+ const vendorIds = newOrderResponse.included.filter((item) => item.type === 'vendor').map((vendor) => vendor.id)
413
+
414
+ return [...new Set(vendorIds)].length > 1
415
+ }
416
+
417
+ buildLineItems(newOrderResponse) {
418
+ return [
419
+ { name: 'Subtotal', amount: newOrderResponse.data.attributes.subtotal_cents },
420
+ { name: 'Shipping', amount: newOrderResponse.data.attributes.ship_total_cents },
421
+ this.buildStoreCreditLine(newOrderResponse),
422
+ this.buildDiscountLine(newOrderResponse),
423
+ this.buildTaxLine(newOrderResponse)
424
+ ].filter((i) => i)
425
+ }
426
+
427
+ buildStoreCreditLine(newOrderResponse) {
428
+ const amount = newOrderResponse.data.attributes.store_credit_total_cents
429
+
430
+ if (amount > 0) {
431
+ return { name: 'Store credit', amount: -amount }
432
+ }
433
+ }
434
+
435
+ buildDiscountLine(newOrderResponse) {
436
+ const amount = newOrderResponse.data.attributes.promo_total_cents
437
+
438
+ if (amount > 0) {
439
+ return { name: 'Discount', amount: -amount }
440
+ }
441
+ }
442
+
443
+ buildTaxLine(newOrderResponse) {
444
+ const attributes = newOrderResponse.data.attributes
445
+ const isTaxIncluded = parseInt(attributes.included_tax_total) > 0
446
+ if (isTaxIncluded) return null
447
+
448
+ const amount = attributes.tax_total_cents
449
+
450
+ return { name: 'Tax', amount: amount }
451
+ }
452
+ }