spree_stripe 1.2.7 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e28c5ddd2fd7eb2275fa2849ffc6a5eb72c741877f5ebcacf5486d9bfd873eb4
4
- data.tar.gz: e7486c7ae9c566a8362d9a6031be7ba6ac43e5d655a59d7527a6cb6d769a9131
3
+ metadata.gz: 9f99142efe129494bc6d2bd1690e85dfae817eb652833217b24d14055811d778
4
+ data.tar.gz: 9916be21b249554ec30e9b46e014634626a2763d1881d8808b14eb87079bc228
5
5
  SHA512:
6
- metadata.gz: ae13960a5deba6d50b233e4a8a659ee26ab90a6db2e2706c5a6773fb0599951b5d957c654f1b2561153bf65abe89aba3aeb375b865fad22c1751238a7a0699b0
7
- data.tar.gz: d0d51d696f41240dfe9809e17c20960302469e3d5451101a317ad95db63053fd1958225531ad334dcd1e5b73929bf006a9dbe455da2aa3dbce719833fb9bb6a9
6
+ metadata.gz: 5791e797836bf0b150915a01b61649ca3da6e5b4f758ec446842d1503919c59f4aaabc09767651e50e5d7c4deaa9bdda3bdb491529bbed20226331cca7fb15bf
7
+ data.tar.gz: 9ec61e460685afd67d0f19aaba2af8d5a33716397e9f56d9939b4b24020cd569a7d11322e7489345bd0be8b1250d777bc69bead40e944de120db58177ea6cec2
@@ -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
@@ -271,6 +271,43 @@ module SpreeStripe
271
271
  end
272
272
  end
273
273
 
274
+ def create_tax_calculation(order)
275
+ protect_from_error do
276
+ send_request do
277
+ Stripe::Tax::Calculation.create(
278
+ SpreeStripe::TaxPresenter.new(order: order).call
279
+ )
280
+ end
281
+ end
282
+ end
283
+
284
+ def create_tax_transaction(payment_intent_id, tax_calculation_id)
285
+ protect_from_error do
286
+ payload = {
287
+ calculation: tax_calculation_id,
288
+ reference: payment_intent_id,
289
+ expand: ['line_items']
290
+ }
291
+
292
+ send_request { Stripe::Tax::Transaction.create_from_calculation(payload) }
293
+ end
294
+ end
295
+
296
+ def attach_customer_to_credit_card(user)
297
+ payment_method_id = user&.default_credit_card&.gateway_payment_profile_id
298
+ return if payment_method_id.blank? || user&.default_credit_card&.gateway_customer_profile_id.present?
299
+
300
+ customer = fetch_or_create_customer(user: user)
301
+ return if customer.blank?
302
+
303
+ send_request { Stripe::PaymentMethod.attach(payment_method_id, { customer: customer.profile_id }) }
304
+
305
+ user.default_credit_card.update(gateway_customer_profile_id: customer.profile_id, gateway_customer_id: customer.id)
306
+ rescue Stripe::StripeError => e
307
+ Rails.error.report(e, context: { payment_method_id: id, user_id: user.id }, source: 'spree_stripe')
308
+ nil
309
+ end
310
+
274
311
  def apple_domain_association_file_content
275
312
  @apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
276
313
  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
 
@@ -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
@@ -97,7 +88,5 @@ module SpreeStripe
97
88
  confirm: @off_session # confirm is required for off_session payments
98
89
  }
99
90
  end
100
-
101
- attr_reader :order, :amount, :customer, :ship_address, :payment_method_id
102
91
  end
103
92
  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
@@ -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'
@@ -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.7'.freeze
2
+ VERSION = '1.3.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,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.7
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
11
+ date: 2025-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -197,16 +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
- - app/models/spree_stripe/address_decorator.rb
209
210
  - app/models/spree_stripe/base.rb
211
+ - app/models/spree_stripe/calculators/stripe_tax.rb
210
212
  - app/models/spree_stripe/credit_card_decorator.rb
211
213
  - app/models/spree_stripe/custom_domain_decorator.rb
212
214
  - app/models/spree_stripe/gateway.rb
@@ -225,11 +227,14 @@ files:
225
227
  - app/models/spree_stripe/payment_sources/link.rb
226
228
  - app/models/spree_stripe/payment_sources/przelewy24.rb
227
229
  - app/models/spree_stripe/payment_sources/sepa_debit.rb
230
+ - app/models/spree_stripe/shipment_decorator.rb
228
231
  - app/models/spree_stripe/store_decorator.rb
229
232
  - app/models/spree_stripe/user_decorator.rb
230
233
  - app/models/spree_stripe/webhook_key.rb
231
234
  - app/presenters/spree_stripe/customer_presenter.rb
232
235
  - app/presenters/spree_stripe/payment_intent_presenter.rb
236
+ - app/presenters/spree_stripe/statement_descriptor_suffix_presenter.rb
237
+ - app/presenters/spree_stripe/tax_presenter.rb
233
238
  - app/serializers/spree/api/v2/platform/affirm_serializer.rb
234
239
  - app/serializers/spree/api/v2/platform/after_pay_serializer.rb
235
240
  - app/serializers/spree/api/v2/platform/alipay_serializer.rb
@@ -1,17 +0,0 @@
1
- module SpreeStripe
2
- module AddressDecorator
3
- def self.prepended(base)
4
- base.after_update :update_stripe_customer, if: :should_update_stripe_customer?
5
- end
6
-
7
- delegate :update_stripe_customer, to: :user, allow_nil: true
8
-
9
- private
10
-
11
- def should_update_stripe_customer?
12
- user_default_billing? && previous_changes.any?
13
- end
14
- end
15
- end
16
-
17
- Spree::Address.prepend(SpreeStripe::AddressDecorator)