spree_stripe 1.2.8 → 1.4.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/images/payment_icons/banktransfer.svg +3 -0
  4. data/app/assets/images/payment_icons/sepadebit.svg +22 -0
  5. data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +1 -1
  6. data/app/controllers/spree_stripe/payment_intents_controller.rb +1 -1
  7. data/app/helpers/spree_stripe/checkout_helper_decorator.rb +15 -0
  8. data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +2 -0
  9. data/app/javascript/spree_stripe/controllers/stripe_controller.js +4 -2
  10. data/app/jobs/spree_stripe/create_tax_transaction_job.rb +10 -0
  11. data/app/models/spree_stripe/calculators/stripe_tax.rb +96 -0
  12. data/app/models/spree_stripe/gateway.rb +78 -4
  13. data/app/models/spree_stripe/order_decorator.rb +14 -0
  14. data/app/models/spree_stripe/payment_decorator.rb +2 -2
  15. data/app/models/spree_stripe/payment_intent.rb +16 -1
  16. data/app/models/spree_stripe/payment_method_decorator.rb +1 -1
  17. data/app/models/spree_stripe/payment_sources/bank_transfer.rb +17 -0
  18. data/app/models/spree_stripe/payment_sources/sepa_debit.rb +4 -0
  19. data/app/models/spree_stripe/shipment_decorator.rb +21 -0
  20. data/app/presenters/spree_stripe/payment_intent_presenter.rb +8 -16
  21. data/app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb +35 -0
  22. data/app/presenters/spree_stripe/tax_presenter.rb +63 -0
  23. data/app/services/spree_stripe/complete_order.rb +9 -3
  24. data/app/services/spree_stripe/create_payment.rb +9 -0
  25. data/app/services/spree_stripe/create_source.rb +2 -0
  26. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +1 -4
  27. data/app/views/spree_stripe/_quick_checkout.html.erb +2 -1
  28. data/config/initializers/spree.rb +1 -0
  29. data/config/locales/en.yml +1 -0
  30. data/lib/spree_stripe/engine.rb +5 -3
  31. data/lib/spree_stripe/version.rb +1 -1
  32. data/lib/spree_stripe.rb +3 -1
  33. metadata +12 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba042a8962210e5a72ea878c0655a7cdc74fe70893d2c702e781e3d32364b8a9
4
- data.tar.gz: b03960a8a7f5d7a2bc45884a3785264bfa7a9ae37bd3620d4e8fcd76a5dc78d8
3
+ metadata.gz: ed3fcc600ad3010c438e0fd42e964612f151d1bfacf1fab96bc781ca2e5d2fe2
4
+ data.tar.gz: c57032392b824d12f83aa4b63bc452cfdd88d22da8a88452426d150cddb226a4
5
5
  SHA512:
6
- metadata.gz: 482a3764301fcb7ecb54b54d28d179999562a3131d3a049d5d61254609226b4044be3edcdc4340d16e5a2971da671f5108761484b4b989648feec1004c3451b0
7
- data.tar.gz: 8c42847f31556185b30ae8f07a97720793dccae143db8cae35b20c990cde396ed027a6f059d56c7198a81124b337408b24898b1ae001387f35fcf0afffd12570
6
+ metadata.gz: f39b40bd6e0ceceb9f152fe4f2f7a2337368bd41bb823bbd1a403a2d126bc2b5c4446e82cd288758f93c422c8360cab41fb6f92bfeeb73ac5eb4ca0dc2ff4c5d
7
+ data.tar.gz: 8464c1cbbc5d25fb74390cd500f1ec8a7dddf2615d2281407ae8fe3ab361b5738e11aa1223d2c9c75a76a991122eba0f248f5152706b645aad44ee3acf985686
data/README.md CHANGED
@@ -140,7 +140,7 @@ We’re thrilled to unveil [Spree 5](https://spreecommerce.org/announcing-spree-
140
140
  - New integrations: a native [Stripe integration](https://github.com/spree/spree_stripe), and also Stripe Connect, Klaviyo integrations available with the Enterprise Edition
141
141
  - Enterprise Edition Admin Features: Audit Log, [Multi-Vendor Marketplace](https://spreecommerce.org/marketplace-ecommerce/), [Multi-tenant / White-label SaaS eCommerce](https://spreecommerce.org/multi-tenant-white-label-ecommerce/)
142
142
 
143
- Read the [full Spree 5 announcement here](https://spreecommerce.org/announcing-spree-5-the-biggest-open-source-release-ever/).
143
+ Read the [full Spree 5 announcement here](https://spreecommerce.org/announcing-spree-5-the-biggest-open-source-release-ever/) and the [changelog of Spree 5](https://github.com/spree/spree/releases) followup improvements.
144
144
 
145
145
  Check out the [Spree 5 demo](https://demo.spreecommerce.org/) for yourself, including this Stripe integration.
146
146
 
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
3
+ </svg>
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" id="katman_1" x="0px" y="0px" style="enable-background:new 0 0 1190.55 841.89;" xml:space="preserve" viewBox="238.65 160 712.7 521">
2
+ <style type="text/css">
3
+ .st0{fill:#26367D;}
4
+ .st1{clip-path:url(#SVGID_00000165933728503162618370000013584018841918814628_);}
5
+ .st2{fill:#FFFFFF;}
6
+ .st3{fill:#FBBA2C;}
7
+ </style>
8
+ <path class="st0" d="M302,160h586c34.99,0,63.35,28.36,63.35,63.35v394.3c0,34.99-28.36,63.35-63.35,63.35H302 c-34.99,0-63.35-28.36-63.35-63.35v-394.3C238.65,188.36,267.01,160,302,160z"/>
9
+ <g>
10
+ <defs>
11
+ <rect id="SVGID_1_" x="341.04" y="162.96" width="507.9" height="506.81"/>
12
+ </defs>
13
+ <clipPath id="SVGID_00000137126528663880082090000017192719521453020821_">
14
+ <use xlink:href="#SVGID_1_" style="overflow:visible;"/>
15
+ </clipPath>
16
+ <g style="clip-path:url(#SVGID_00000137126528663880082090000017192719521453020821_);">
17
+ <path class="st0" d="M367.09,226.31h455.81c14.38,0,26.04,13.62,26.04,30.41v319.29c0,16.79-11.66,30.41-26.04,30.41H367.09 c-14.38,0-26.05-13.62-26.05-30.41V256.72C341.04,239.93,352.71,226.31,367.09,226.31z"/>
18
+ <path class="st2" d="M404.53,381.92c0,3.22,0,5.34,1.06,8.55c0,1.09,1.05,2.12,1.05,3.22c2.06,13.91,8.3,20.27,21.76,21.38 c9.3,1.08,19.64,2.12,29,3.22c5.18,0,8.3,3.22,8.3,9.58c0,6.38-2.06,10.69-8.3,10.69h-18.64c-6.24,0-8.3-3.22-9.3-12.81h-24.82 v9.66c0,1.08,1.06,3.22,1.06,4.24c1.05,9.6,7.24,16.03,16.58,18.15c6.22,1.09,12.4,1.09,18.64,2.12h18.64 c6.24-1.09,11.4-2.12,17.58-4.24c7.24-2.12,12.42-7.48,13.46-16.03c1.06-7.48,1.06-16.03,1.06-23.5 c-1.05-8.55-5.16-14.94-13.46-17.1l-18.64-3.23c-6.22-1.09-12.42-1.09-18.64-1.09c-7.24,0-9.3-3.22-9.3-9.58 c0-7.48,3.12-9.6,9.3-10.69h10.34c9.3,0,11.4,1.11,12.42,10.69v1.09h24.82v-6.43c0-13.91-6.22-22.47-20.7-24.6 c-14.46-2.12-28.98-2.12-43.46,1.09c-11.4,2.12-16.58,7.48-18.64,20.27C405.55,378.69,404.53,379.8,404.53,381.92z M631.19,458.83 v-27.8h35.18c13.46,0,22.76-8.55,24.82-21.38c1.06-8.55,2.06-17.1,1.06-25.61c-1.06-19.24-10.34-28.82-28.98-29.93 c-17.58-1.09-35.18-1.09-52.82-1.09c-4.12,0-5.18,1.11-5.18,5.35v101.5C613.61,459.92,622.91,459.92,631.19,458.83z M631.19,407.53V377.6c0,0,2.06-2.12,3.12-2.12h18.64c7.24,1.11,10.36,4.32,10.36,11.78v10.69c0,6.43-2.06,9.58-8.3,9.58H631.19z M708.83,459.92c1.06-5.34,2.06-11.77,4.18-17.1c0-1.11,2.06-2.14,3.12-2.14h33.1c1.06,0,2.06,2.12,3.12,3.22 c2.06,5.35,3.12,10.69,5.18,16.03h27.94c-1.06-2.12-2.06-5.34-3.12-7.46l-27.94-96.23c0-1.09-2.06-3.22-3.12-3.22h-39.34 c-4.18,12.88-7.24,25.67-11.4,38.49c-6.24,22.41-13.46,45.9-19.64,68.37C680.9,459.87,708.83,459.92,708.83,459.92z M747.12,421.43h-26.94c4.12-15,8.3-31.01,12.42-45.96h1.05c4.18,15,9.34,29.93,13.46,45.96H747.12z"/>
19
+ <path class="st3" d="M575.35,441.71c-16.6,0-29-7.46-34.18-19.24h37.28l4.12-11.77h-45.58v-8.55l46.59,2.12l5.16-13.86h-49.68 c5.16-14.94,18.64-22.46,36.22-22.46c7.44-0.05,14.81,1.44,21.76,4.26l4.12-14.94c-4.12-2.14-13.46-4.26-26.92-4.26 c-27.95,0-49.71,13.91-56.94,38.49h-12.35l-5.18,11.78h15.52v9.53h-9.3l-6.22,11.78h17.58c6.24,21.37,25.88,34.18,53.82,34.18 c13.46,0,23.82-2.12,27.94-4.26l-3.12-14.94C591.93,440.69,583.63,441.71,575.35,441.71z"/>
20
+ </g>
21
+ </g>
22
+ </svg>
@@ -56,7 +56,7 @@ module Spree
56
56
  render_error_payload(Spree.t('order_already_completed'))
57
57
  elsif order != spree_current_order
58
58
  raise ActiveRecord::RecordNotFound
59
- elsif stripe_payment_intent.status == 'succeeded'
59
+ elsif @payment_intent.accepted?
60
60
  spree_authorize! :update, spree_current_order, order_token
61
61
 
62
62
  SpreeStripe::CompleteOrder.new(payment_intent: @payment_intent).call
@@ -26,7 +26,7 @@ module SpreeStripe
26
26
  elsif @order.completed?
27
27
  redirect_to spree.checkout_complete_path(@order.token), status: :see_other
28
28
  # if the payment intent is successful, we need to process the payment and complete the order
29
- elsif @stripe_payment_intent.status == 'succeeded'
29
+ elsif @payment_intent_record.accepted?
30
30
  @order = SpreeStripe::CompleteOrder.new(payment_intent: @payment_intent_record).call
31
31
 
32
32
  # set the session flag to indicate that the order was placed now
@@ -0,0 +1,15 @@
1
+ module SpreeStripe
2
+ module CheckoutHelperDecorator
3
+ def checkout_payment_sources(payment_method = nil)
4
+ payment_sources = super(payment_method)
5
+ return payment_sources if payment_sources.empty? || !payment_method.stripe?
6
+
7
+ wallet_sources, non_wallet_sources = payment_sources.partition { |source| source.wallet_type.present? }
8
+ unique_wallet_sources = wallet_sources.sort_by(&:created_at).reverse.uniq { |source| [source.wallet_type, source.cc_type, source.last_digits, source.month, source.year] }
9
+
10
+ non_wallet_sources + unique_wallet_sources
11
+ end
12
+ end
13
+ end
14
+
15
+ Spree::CheckoutHelper.prepend(SpreeStripe::CheckoutHelperDecorator)
@@ -25,6 +25,7 @@ export default class extends Controller {
25
25
  checkoutValidateOrderForPaymentPath: String,
26
26
  shippingRequired: { type: Boolean, default: true },
27
27
  returnUrl: String,
28
+ phoneRequired: { type: Boolean, default: false },
28
29
  }
29
30
 
30
31
  static targets = ['container']
@@ -104,6 +105,7 @@ export default class extends Controller {
104
105
 
105
106
  event.resolve({
106
107
  emailRequired: true,
108
+ phoneNumberRequired: this.phoneRequiredValue,
107
109
  shippingAddressRequired: this.shippingRequiredValue,
108
110
  allowedShippingCountries: this.availableCountriesValue,
109
111
  // 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
@@ -13,7 +13,8 @@ export default class extends Controller {
13
13
  colorText: String,
14
14
  paymentIntentPath: String,
15
15
  checkoutPath: String,
16
- checkoutValidateOrderForPaymentPath: String
16
+ checkoutValidateOrderForPaymentPath: String,
17
+ paymentElementAdditionalOptions: { type: Object, default: {} }
17
18
  }
18
19
 
19
20
  static targets = [
@@ -96,7 +97,8 @@ export default class extends Controller {
96
97
  postalCode: 'never'
97
98
  }
98
99
  }
99
- }
100
+ },
101
+ ...this.paymentElementAdditionalOptionsValue
100
102
  })
101
103
  paymentElement.mount(this.paymentElementTarget)
102
104
  paymentElement.on('change', (event) => {
@@ -0,0 +1,10 @@
1
+ module SpreeStripe
2
+ class CreateTaxTransactionJob < BaseJob
3
+ def perform(store_id, payment_intent_id, tax_calculation_id)
4
+ store = Spree::Store.find(store_id)
5
+ gateway = store.stripe_gateway
6
+
7
+ gateway.create_tax_transaction(payment_intent_id, tax_calculation_id) if gateway.present?
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,96 @@
1
+ module SpreeStripe
2
+ module Calculators
3
+ class StripeTax < ::Spree::Calculator
4
+ def self.description
5
+ 'Stripe tax calculator'
6
+ end
7
+
8
+ def compute_line_item(line_item)
9
+ order = line_item.order
10
+ return 0.to_d unless stripe_tax_enabled?(order)
11
+
12
+ tax_calculation_data = get_or_create_tax_calculation(order)
13
+ return 0.to_d unless tax_calculation_data.present?
14
+
15
+ # Find the tax line for this specific line item
16
+ line_items = tax_calculation_data.dig('line_items', 'data') || []
17
+ stripe_line_item = line_items.find do |line|
18
+ line['reference'].to_s == line_item.id.to_s
19
+ end
20
+
21
+ # Debug logging
22
+ if stripe_line_item.nil?
23
+ Rails.logger.debug "Stripe Tax: No line item found for reference #{line_item.id}. Available references: #{line_items.map { |li| li['reference'] }.join(', ')}"
24
+ return 0.to_d
25
+ end
26
+
27
+ # Get the tax amount for this line item from amount_tax field
28
+ amount_in_cents = stripe_line_item['amount_tax'] || 0
29
+ Rails.logger.debug "Stripe Tax: Line item #{line_item.id} tax amount: #{amount_in_cents} cents"
30
+ amount_money = Spree::Money.from_cents(amount_in_cents, { currency: order.currency })
31
+ amount_money.to_d
32
+ end
33
+
34
+ def compute_shipment(shipment)
35
+ order = shipment.order
36
+ return 0.to_d unless stripe_tax_enabled?(order)
37
+
38
+ tax_calculation_data = get_or_create_tax_calculation(order)
39
+ return 0.to_d unless tax_calculation_data.present?
40
+
41
+ # Get the shipping cost tax breakdown
42
+ shipping_cost = tax_calculation_data['shipping_cost']
43
+ return 0.to_d unless shipping_cost.present?
44
+
45
+ # Get the tax amount for shipping
46
+ amount_in_cents = shipping_cost['amount_tax'] || 0
47
+ amount_money = Spree::Money.from_cents(amount_in_cents, { currency: order.currency })
48
+ amount_money.to_d
49
+ end
50
+
51
+ private
52
+
53
+ def stripe_tax_enabled?(order)
54
+ stripe_gateway = order.store.stripe_gateway
55
+ stripe_gateway.present? && stripe_gateway.active?
56
+ end
57
+
58
+ def get_or_create_tax_calculation(order)
59
+ return unless order.tax_address.present?
60
+ return if order.state.in?(%w[cart address])
61
+
62
+ stripe_gateway = order.store.stripe_gateway
63
+
64
+ cache_key = [
65
+ order.number,
66
+ order.currency,
67
+ order.item_total,
68
+ order.item_count,
69
+ order.line_items.reload.map { |li| [li.id, li.quantity, li.taxable_amount].compact }.flatten.join('_'),
70
+ order.shipments.map { |s| [s.shipping_method&.id, s.cost].compact }.flatten.join('_'),
71
+ order.tax_address&.cache_key_with_version,
72
+ ].compact.join('_')
73
+
74
+ tax_calculation = Rails.cache.fetch("stripe_tax_calculation_#{cache_key}", expires_in: 1.hour) do
75
+ stripe_gateway.create_tax_calculation(order).to_hash.deep_stringify_keys
76
+ rescue => e
77
+ Rails.error.report(e, context: { order_id: order.id }, source: 'spree_stripe')
78
+ {}
79
+ end
80
+
81
+ return nil unless tax_calculation.present?
82
+
83
+ # persist the tax calculation in the order for future use
84
+ # we will need this to create tax transaction
85
+ order.stripe_tax_calculation_id = tax_calculation['id']
86
+ order.save!
87
+
88
+ tax_calculation
89
+ rescue => e
90
+ Rails.error.report(e, context: { order_id: order.id }, source: 'spree_stripe')
91
+ nil
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -2,6 +2,9 @@ module SpreeStripe
2
2
  class Gateway < ::Spree::Gateway
3
3
  include SpreeStripe::Gateway::Tax if defined?(SpreeStripe::Gateway::Tax)
4
4
 
5
+ DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = %w[sepa_debit us_bank_account].freeze
6
+ BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
7
+
5
8
  preference :publishable_key, :password
6
9
  preference :secret_key, :password
7
10
 
@@ -23,6 +26,28 @@ module SpreeStripe
23
26
  self.class
24
27
  end
25
28
 
29
+ def payment_intent_accepted?(payment_intent)
30
+ payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
31
+ end
32
+
33
+ def payment_intent_delayed_notification?(payment_intent)
34
+ payment_method = payment_intent.payment_method
35
+ return false unless payment_method.respond_to?(:type)
36
+
37
+ payment_intent.payment_method.type.in?(DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES)
38
+ end
39
+
40
+ def payment_intent_charge_not_required?(payment_intent)
41
+ payment_intent_bank_payment_method?(payment_intent)
42
+ end
43
+
44
+ def payment_intent_bank_payment_method?(payment_intent)
45
+ payment_method = payment_intent.payment_method
46
+ return false unless payment_method.respond_to?(:type)
47
+
48
+ payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
49
+ end
50
+
26
51
  # @param amount_in_cents [Integer] the amount in cents to capture
27
52
  # @param payment_source [Spree::CreditCard | Spree::PaymentSource]
28
53
  # @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
@@ -52,7 +77,7 @@ module SpreeStripe
52
77
  payment = ensure_payment_intent_exists_for_payment(payment, amount_in_cents, payment_source)
53
78
  stripe_payment_intent = retrieve_payment_intent(payment.response_code)
54
79
 
55
- response = if stripe_payment_intent.status == 'succeeded'
80
+ response = if payment_intent_accepted?(stripe_payment_intent)
56
81
  # payment intent is already confirmed via Stripe JS SDK
57
82
  stripe_payment_intent
58
83
  else
@@ -92,8 +117,13 @@ module SpreeStripe
92
117
  end
93
118
  end
94
119
 
95
- def void(response_code, source, gateway_options)
96
- raise NotImplementedError
120
+ def void(response_code, _source, _gateway_options)
121
+ return failure('Response code is blank') if response_code.blank?
122
+
123
+ protect_from_error do
124
+ response = cancel_payment_intent(response_code)
125
+ success(response.id, response)
126
+ end
97
127
  end
98
128
 
99
129
  def cancel(payment_intent_id, payment = nil)
@@ -205,7 +235,7 @@ module SpreeStripe
205
235
  end
206
236
 
207
237
  def retrieve_payment_intent(payment_intent_id)
208
- send_request { Stripe::PaymentIntent.retrieve(payment_intent_id) }
238
+ send_request { Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }) }
209
239
  end
210
240
 
211
241
  def confirm_payment_intent(payment_intent_id)
@@ -271,6 +301,43 @@ module SpreeStripe
271
301
  end
272
302
  end
273
303
 
304
+ def create_tax_calculation(order)
305
+ protect_from_error do
306
+ send_request do
307
+ Stripe::Tax::Calculation.create(
308
+ SpreeStripe::TaxPresenter.new(order: order).call
309
+ )
310
+ end
311
+ end
312
+ end
313
+
314
+ def create_tax_transaction(payment_intent_id, tax_calculation_id)
315
+ protect_from_error do
316
+ payload = {
317
+ calculation: tax_calculation_id,
318
+ reference: payment_intent_id,
319
+ expand: ['line_items']
320
+ }
321
+
322
+ send_request { Stripe::Tax::Transaction.create_from_calculation(payload) }
323
+ end
324
+ end
325
+
326
+ def attach_customer_to_credit_card(user)
327
+ payment_method_id = user&.default_credit_card&.gateway_payment_profile_id
328
+ return if payment_method_id.blank? || user&.default_credit_card&.gateway_customer_profile_id.present?
329
+
330
+ customer = fetch_or_create_customer(user: user)
331
+ return if customer.blank?
332
+
333
+ send_request { Stripe::PaymentMethod.attach(payment_method_id, { customer: customer.profile_id }) }
334
+
335
+ user.default_credit_card.update(gateway_customer_profile_id: customer.profile_id, gateway_customer_id: customer.id)
336
+ rescue Stripe::StripeError => e
337
+ Rails.error.report(e, context: { payment_method_id: id, user_id: user.id }, source: 'spree_stripe')
338
+ nil
339
+ end
340
+
274
341
  def apple_domain_association_file_content
275
342
  @apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
276
343
  end
@@ -382,5 +449,12 @@ module SpreeStripe
382
449
 
383
450
  SpreeStripe::CustomerPresenter.new(name: name, email: email, address: address).call
384
451
  end
452
+
453
+ def payment_intent_accepted_statuses(payment_intent)
454
+ statuses = %w[succeeded]
455
+ statuses << 'processing' if payment_intent_delayed_notification?(payment_intent)
456
+ statuses << 'requires_action' if payment_intent_charge_not_required?(payment_intent)
457
+ statuses
458
+ end
385
459
  end
386
460
  end
@@ -2,6 +2,8 @@ module SpreeStripe
2
2
  module OrderDecorator
3
3
  def self.prepended(base)
4
4
  base.has_many :payment_intents, class_name: 'SpreeStripe::PaymentIntent', dependent: :destroy
5
+
6
+ base.store_accessor :private_metadata, :stripe_tax_calculation_id
5
7
  end
6
8
 
7
9
  def update_payment_intents
@@ -23,6 +25,18 @@ module SpreeStripe
23
25
 
24
26
  payment_intents.update_all(customer_id: customer_id) if customer_id.present?
25
27
  end
28
+
29
+ def persist_user_credit_card
30
+ super
31
+ stripe_gateway = store.stripe_gateway
32
+ return if stripe_gateway.blank?
33
+
34
+ stripe_gateway.attach_customer_to_credit_card(user)
35
+ end
36
+
37
+ def stripe_payment_intent
38
+ @stripe_payment_intent ||= payment_intents.last.stripe_payment_intent
39
+ end
26
40
  end
27
41
  end
28
42
 
@@ -14,13 +14,13 @@ module SpreeStripe
14
14
  'pass' => 'P',
15
15
  'unchecked' => 'I'
16
16
  }
17
- }.freeze
17
+ }.freeze unless defined?(AVS_CODES)
18
18
 
19
19
  CVV_CODES = {
20
20
  'pass' => 'M',
21
21
  'fail' => 'N',
22
22
  'unchecked' => 'P'
23
- }.freeze
23
+ }.freeze unless defined?(CVV_CODES)
24
24
 
25
25
  def self.prepended(base)
26
26
  base.store_accessor :private_metadata, :stripe_charge_id
@@ -26,12 +26,27 @@ module SpreeStripe
26
26
  delegate :api_options, to: :payment_method
27
27
  delegate :store, :currency, to: :order
28
28
 
29
+ def accepted?
30
+ payment_method.payment_intent_accepted?(stripe_payment_intent)
31
+ end
32
+
33
+ def successful?
34
+ stripe_payment_intent.status == 'succeeded'
35
+ end
36
+
37
+ def charge_not_required?
38
+ payment_method.payment_intent_charge_not_required?(stripe_payment_intent)
39
+ end
40
+
29
41
  def stripe_payment_intent
30
42
  @stripe_payment_intent ||= payment_method.retrieve_payment_intent(stripe_id)
31
43
  end
32
44
 
33
45
  def stripe_charge
34
- @stripe_charge ||= payment_method.retrieve_charge(stripe_payment_intent.latest_charge)
46
+ @stripe_charge ||= begin
47
+ latest_charge = stripe_payment_intent.latest_charge
48
+ latest_charge.present? ? payment_method.retrieve_charge(latest_charge) : nil
49
+ end
35
50
  end
36
51
 
37
52
  # here we create a payment if it doesn't exist
@@ -1,6 +1,6 @@
1
1
  module SpreeStripe
2
2
  module PaymentMethodDecorator
3
- STRIPE_TYPE = 'SpreeStripe::Gateway'.freeze
3
+ STRIPE_TYPE = 'SpreeStripe::Gateway'.freeze unless defined?(STRIPE_TYPE)
4
4
 
5
5
  def self.prepended(base)
6
6
  base.has_many :payment_methods_webhook_keys, class_name: 'SpreeStripe::PaymentMethodsWebhookKey'
@@ -0,0 +1,17 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class BankTransfer < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+
8
+ def self.display_name
9
+ Spree.t(:bank_transfer)
10
+ end
11
+
12
+ def name
13
+ Spree.t(:bank_transfer)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -8,6 +8,10 @@ module SpreeStripe
8
8
  def self.display_name
9
9
  'SEPA Debit'
10
10
  end
11
+
12
+ def name
13
+ 'SEPA Debit'
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,21 @@
1
+ module SpreeStripe
2
+ module ShipmentDecorator
3
+ def self.prepended(base)
4
+ base.state_machine.after_transition to: :shipped, do: :create_tax_transaction_async
5
+ end
6
+
7
+ private
8
+
9
+ def create_tax_transaction_async
10
+ return unless order.fully_shipped?
11
+ return if order.stripe_tax_calculation_id.blank?
12
+
13
+ payment_intent = order.payment_intents.last
14
+ return if payment_intent.blank?
15
+
16
+ SpreeStripe::CreateTaxTransactionJob.perform_later(order.store_id, payment_intent.stripe_id, order.stripe_tax_calculation_id)
17
+ end
18
+ end
19
+ end
20
+
21
+ Spree::Shipment.prepend(SpreeStripe::ShipmentDecorator) if Spree::Shipment.included_modules.exclude?(SpreeStripe::ShipmentDecorator)
@@ -1,7 +1,6 @@
1
1
  module SpreeStripe
2
2
  class PaymentIntentPresenter
3
3
  SETUP_FUTURE_USAGE = 'off_session'
4
- STATEMENT_DESCRIPTOR_MAX_CHARACTERS = 22
5
4
 
6
5
  def initialize(amount:, order:, customer: nil, payment_method_id: nil, off_session: false)
7
6
  @amount = amount
@@ -50,12 +49,14 @@ module SpreeStripe
50
49
 
51
50
  private
52
51
 
52
+ attr_reader :order, :amount, :customer, :ship_address, :payment_method_id
53
+
53
54
  def basic_payload
54
55
  {
55
56
  amount: amount,
56
57
  customer: customer,
57
58
  currency: order.currency,
58
- statement_descriptor_suffix: statement_descriptor,
59
+ statement_descriptor_suffix: statement_descriptor_suffix,
59
60
  automatic_payment_methods: {
60
61
  enabled: true
61
62
  },
@@ -66,18 +67,8 @@ module SpreeStripe
66
67
  }
67
68
  end
68
69
 
69
- def statement_descriptor
70
- billing_name = order.store.billing_name
71
- descriptor = "#{order.number} #{billing_name}".strip
72
-
73
- if descriptor.length > STATEMENT_DESCRIPTOR_MAX_CHARACTERS
74
- additional_char_count = descriptor.length - STATEMENT_DESCRIPTOR_MAX_CHARACTERS + 1
75
- short_billing_name = billing_name[0...-additional_char_count]
76
-
77
- descriptor = "#{order.number} #{short_billing_name}".strip
78
- end
79
-
80
- descriptor
70
+ def statement_descriptor_suffix
71
+ SpreeStripe::StatementDescriptorSuffixPresenter.new(order_description: order.number).call
81
72
  end
82
73
 
83
74
  def new_payment_method_payload
@@ -85,6 +76,9 @@ module SpreeStripe
85
76
  payment_method_options: {
86
77
  card: {
87
78
  setup_future_usage: SETUP_FUTURE_USAGE
79
+ },
80
+ sepa_debit: {
81
+ setup_future_usage: SETUP_FUTURE_USAGE
88
82
  }
89
83
  }
90
84
  }
@@ -97,7 +91,5 @@ module SpreeStripe
97
91
  confirm: @off_session # confirm is required for off_session payments
98
92
  }
99
93
  end
100
-
101
- attr_reader :order, :amount, :customer, :ship_address, :payment_method_id
102
94
  end
103
95
  end
@@ -0,0 +1,35 @@
1
+ module SpreeStripe
2
+ class StatementDescriptorSuffixPresenter
3
+ STATEMENT_DESCRIPTOR_MAX_CHARS = 10
4
+ STATEMENT_DESCRIPTOR_NOT_ALLOWED_CHARS = /[<>'"*\\]/.freeze
5
+ STATEMENT_PREFIX = 'O#'
6
+
7
+ def initialize(order_description:)
8
+ @order_description = order_description.to_s
9
+ end
10
+
11
+ def call
12
+ return if stripped_order_description.blank?
13
+
14
+ if stripped_order_description.count("A-Z") > 0
15
+ stripped_order_description
16
+ else
17
+ "#{STATEMENT_PREFIX}#{stripped_order_description}"[0...STATEMENT_DESCRIPTOR_MAX_CHARS].strip
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :order_description
24
+
25
+ def stripped_order_description
26
+ @stripped_order_description ||= begin
27
+ AnyAscii.transliterate(order_description)
28
+ .gsub(STATEMENT_DESCRIPTOR_NOT_ALLOWED_CHARS, '')
29
+ .strip
30
+ .upcase[0...STATEMENT_DESCRIPTOR_MAX_CHARS]
31
+ .strip
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module SpreeStripe
2
+ class TaxPresenter
3
+ SHIPPING_TAX_CODE = 'txcd_92010001'.freeze
4
+ TAX_BEHAVIOR = 'exclusive'.freeze
5
+
6
+ def initialize(order:)
7
+ @order = order
8
+ end
9
+
10
+ attr_reader :order
11
+
12
+ def call
13
+ {
14
+ currency: order.currency.downcase,
15
+ line_items: build_line_items_attributes(order.line_items),
16
+ customer_details: {
17
+ address: build_address_attributes(order.tax_address),
18
+ address_source: 'shipping',
19
+ },
20
+ shipping_cost: {
21
+ amount: calculate_shipping_cost(order.shipments),
22
+ tax_behavior: TAX_BEHAVIOR,
23
+ tax_code: SHIPPING_TAX_CODE
24
+ },
25
+ expand: [
26
+ 'line_items.data.tax_breakdown',
27
+ 'shipping_cost.tax_breakdown'
28
+ ]
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def build_line_items_attributes(line_items)
35
+ line_items.map do |line_item|
36
+ {
37
+ reference: line_item.id,
38
+ tax_behavior: TAX_BEHAVIOR,
39
+ amount: Spree::Money.new(line_item.taxable_amount, currency: order.currency).cents,
40
+ quantity: line_item.quantity
41
+ }
42
+ end
43
+ end
44
+
45
+ def build_address_attributes(address)
46
+ return {} if address.blank?
47
+
48
+ {
49
+ line1: address.address1,
50
+ city: address.city,
51
+ state: address.state_text,
52
+ postal_code: address.zipcode,
53
+ country: address.country_iso
54
+ }
55
+ end
56
+
57
+ def calculate_shipping_cost(shipments)
58
+ zero_money = Spree::Money.new(0, currency: order.currency)
59
+ shipment_money = shipments.sum(zero_money, &:display_cost)
60
+ shipment_money.cents
61
+ end
62
+ end
63
+ end
@@ -9,7 +9,7 @@ module SpreeStripe
9
9
  def call
10
10
  order = payment_intent.order
11
11
 
12
- return order if order.completed? || order.canceled?
12
+ return order if (order.completed? && order.paid?) || order.canceled?
13
13
 
14
14
  charge = payment_intent.stripe_charge
15
15
 
@@ -21,10 +21,14 @@ module SpreeStripe
21
21
  payment = payment_intent.find_or_create_payment!
22
22
 
23
23
  # process the payment
24
- payment.process!
24
+ if payment_intent.successful?
25
+ payment.process!
26
+ else
27
+ payment.authorize!
28
+ end
25
29
 
26
30
  # complete the order
27
- Spree::Dependencies.checkout_complete_service.constantize.call(order: order)
31
+ Spree::Dependencies.checkout_complete_service.constantize.call(order: order) unless order.completed?
28
32
  end
29
33
 
30
34
  order.reload
@@ -34,6 +38,8 @@ module SpreeStripe
34
38
 
35
39
  # we need to perform this for quick checkout orders which do not have these fields filled
36
40
  def add_customer_information(order, charge)
41
+ return order if charge.blank?
42
+
37
43
  billing_details = charge.billing_details
38
44
  address = billing_details.address
39
45
 
@@ -19,6 +19,15 @@ module SpreeStripe
19
19
  stripe_billing_details: stripe_charge.billing_details,
20
20
  gateway: gateway
21
21
  ).call
22
+ elsif payment_intent.charge_not_required?
23
+ stripe_payment_intent = payment_intent.stripe_payment_intent
24
+ source = SpreeStripe::CreateSource.new(
25
+ order: order,
26
+ stripe_payment_method_details: stripe_payment_intent.payment_method,
27
+ stripe_payment_method_id: stripe_payment_intent.payment_method.id,
28
+ stripe_billing_details: nil,
29
+ gateway: gateway
30
+ ).call
22
31
  end
23
32
 
24
33
  # sometimes a job is re-tried and creates a double payment record so we need to avoid it!
@@ -34,6 +34,8 @@ module SpreeStripe
34
34
  SpreeStripe::PaymentSources::Link.create!(source_params)
35
35
  when 'affirm'
36
36
  SpreeStripe::PaymentSources::Affirm.create!(source_params)
37
+ when 'customer_balance', 'us_bank_account'
38
+ SpreeStripe::PaymentSources::BankTransfer.create!(source_params)
37
39
  else
38
40
  raise "[STRIPE] Unknown payment method #{stripe_payment_method_details.type}"
39
41
  end
@@ -2,16 +2,13 @@ module SpreeStripe
2
2
  module WebhookHandlers
3
3
  class PaymentIntentPaymentFailed
4
4
  def call(event)
5
- return unless ['affirm', 'afterpay_clearpay'].include?(event.data.object&.last_payment_error&.payment_method&.type)
6
-
7
5
  payment_intent = SpreeStripe::PaymentIntent.find_by(stripe_id: event.data.object.id)
8
6
  return if payment_intent.nil?
9
7
 
10
8
  order = payment_intent.order
11
-
12
9
  return if order.canceled?
13
10
 
14
- order.cancel!
11
+ order.cancel! if order.can_cancel?
15
12
  end
16
13
  end
17
14
  end
@@ -23,7 +23,8 @@
23
23
  checkout_stripe_button_checkout_validate_gift_card_data_path_value: respond_to?(:validate_gift_card_data_api_v2_storefront_checkout_path) ? spree.validate_gift_card_data_api_v2_storefront_checkout_path : nil,
24
24
  checkout_stripe_button_checkout_validate_order_for_payment_path_value: spree.validate_order_for_payment_api_v2_storefront_checkout_path,
25
25
  checkout_stripe_button_return_url_value: spree.stripe_payment_intent_url(current_stripe_payment_intent),
26
- checkout_stripe_button_shipping_required_value: @order.respond_to?(:quick_checkout_require_address?) ? @order.quick_checkout_require_address? : true
26
+ checkout_stripe_button_shipping_required_value: @order.respond_to?(:quick_checkout_require_address?) ? @order.quick_checkout_require_address? : true,
27
+ checkout_stripe_button_phone_required_value: Spree::Config[:address_requires_phone]
27
28
  } do %>
28
29
  <div id="payment-request-button"></div>
29
30
 
@@ -1,5 +1,6 @@
1
1
  Rails.application.config.after_initialize do
2
2
  Rails.application.config.spree.payment_methods << SpreeStripe::Gateway
3
+ Rails.application.config.spree.calculators.tax_rates << SpreeStripe::Calculators::StripeTax
3
4
 
4
5
  if Rails.application.config.respond_to?(:spree_storefront)
5
6
  Rails.application.config.spree_storefront.head_partials << 'spree_stripe/head'
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  en:
3
3
  spree:
4
+ bank_transfer: Bank Transfer
4
5
  stripe:
5
6
  payment_intent_errors:
6
7
  canceled: Payment intent canceled
@@ -14,9 +14,11 @@ module SpreeStripe
14
14
  end
15
15
 
16
16
  initializer 'spree_stripe.assets' do |app|
17
- app.config.assets.paths << root.join('app/javascript')
18
- app.config.assets.paths << root.join('vendor/javascript')
19
- app.config.assets.precompile += %w[spree_stripe_manifest]
17
+ if app.config.respond_to?(:assets)
18
+ app.config.assets.paths << root.join('app/javascript')
19
+ app.config.assets.paths << root.join('vendor/javascript')
20
+ app.config.assets.precompile += %w[spree_stripe_manifest]
21
+ end
20
22
  end
21
23
 
22
24
  initializer 'spree_stripe.importmap', before: 'importmap' do |app|
@@ -1,5 +1,5 @@
1
1
  module SpreeStripe
2
- VERSION = '1.2.8'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
 
4
4
  def gem_version
5
5
  Gem::Version.new(VERSION)
data/lib/spree_stripe.rb CHANGED
@@ -8,7 +8,9 @@ require 'stripe'
8
8
  require 'stripe_event'
9
9
 
10
10
  module SpreeStripe
11
+ mattr_accessor :queue
12
+
11
13
  def self.queue
12
- 'spree_stripe'
14
+ @@queue ||= Spree.queues.default
13
15
  end
14
16
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.8
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: spree
@@ -178,7 +177,6 @@ dependencies:
178
177
  - - ">="
179
178
  - !ruby/object:Gem::Version
180
179
  version: '0'
181
- description:
182
180
  email: hello@spreecommerce.org
183
181
  executables: []
184
182
  extensions: []
@@ -188,7 +186,9 @@ files:
188
186
  - README.md
189
187
  - Rakefile
190
188
  - app/assets/config/spree_stripe_manifest.js
189
+ - app/assets/images/payment_icons/banktransfer.svg
191
190
  - app/assets/images/payment_icons/link.svg
191
+ - app/assets/images/payment_icons/sepadebit.svg
192
192
  - app/controllers/spree/api/v2/storefront/stripe/base_controller.rb
193
193
  - app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb
194
194
  - app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb
@@ -197,15 +197,18 @@ files:
197
197
  - app/controllers/spree_stripe/store_controller_decorator.rb
198
198
  - app/controllers/stripe_event/webhook_controller_decorator.rb
199
199
  - app/helpers/spree_stripe/base_helper.rb
200
+ - app/helpers/spree_stripe/checkout_helper_decorator.rb
200
201
  - app/javascript/spree_stripe/application.js
201
202
  - app/javascript/spree_stripe/controllers/stripe_button_controller.js
202
203
  - app/javascript/spree_stripe/controllers/stripe_controller.js
203
204
  - app/jobs/spree_stripe/base_job.rb
204
205
  - app/jobs/spree_stripe/complete_order_job.rb
206
+ - app/jobs/spree_stripe/create_tax_transaction_job.rb
205
207
  - app/jobs/spree_stripe/create_webhook_endpoint_job.rb
206
208
  - app/jobs/spree_stripe/register_domain_job.rb
207
209
  - app/jobs/spree_stripe/update_customer_job.rb
208
210
  - app/models/spree_stripe/base.rb
211
+ - app/models/spree_stripe/calculators/stripe_tax.rb
209
212
  - app/models/spree_stripe/credit_card_decorator.rb
210
213
  - app/models/spree_stripe/custom_domain_decorator.rb
211
214
  - app/models/spree_stripe/gateway.rb
@@ -219,16 +222,20 @@ files:
219
222
  - app/models/spree_stripe/payment_sources/affirm.rb
220
223
  - app/models/spree_stripe/payment_sources/after_pay.rb
221
224
  - app/models/spree_stripe/payment_sources/alipay.rb
225
+ - app/models/spree_stripe/payment_sources/bank_transfer.rb
222
226
  - app/models/spree_stripe/payment_sources/ideal.rb
223
227
  - app/models/spree_stripe/payment_sources/klarna.rb
224
228
  - app/models/spree_stripe/payment_sources/link.rb
225
229
  - app/models/spree_stripe/payment_sources/przelewy24.rb
226
230
  - app/models/spree_stripe/payment_sources/sepa_debit.rb
231
+ - app/models/spree_stripe/shipment_decorator.rb
227
232
  - app/models/spree_stripe/store_decorator.rb
228
233
  - app/models/spree_stripe/user_decorator.rb
229
234
  - app/models/spree_stripe/webhook_key.rb
230
235
  - app/presenters/spree_stripe/customer_presenter.rb
231
236
  - app/presenters/spree_stripe/payment_intent_presenter.rb
237
+ - app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb
238
+ - app/presenters/spree_stripe/tax_presenter.rb
232
239
  - app/serializers/spree/api/v2/platform/affirm_serializer.rb
233
240
  - app/serializers/spree/api/v2/platform/after_pay_serializer.rb
234
241
  - app/serializers/spree/api/v2/platform/alipay_serializer.rb
@@ -292,7 +299,6 @@ homepage: https://github.com/spree/spree_stripe
292
299
  licenses:
293
300
  - AGPL-3.0-or-later
294
301
  metadata: {}
295
- post_install_message:
296
302
  rdoc_options: []
297
303
  require_paths:
298
304
  - lib
@@ -308,8 +314,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
308
314
  version: '0'
309
315
  requirements:
310
316
  - none
311
- rubygems_version: 3.5.3
312
- signing_key:
317
+ rubygems_version: 4.0.2
313
318
  specification_version: 4
314
319
  summary: Official Spree Commerce Stripe payment gateway extension
315
320
  test_files: []