spree_core 5.2.5 → 5.2.6

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/addresses_helper.rb +8 -0
  3. data/app/models/concerns/spree/ransackable_attributes.rb +6 -3
  4. data/app/models/spree/line_item.rb +2 -2
  5. data/app/models/spree/order/digital.rb +13 -5
  6. data/app/models/spree/payment/processing.rb +1 -0
  7. data/app/models/spree/policy.rb +9 -0
  8. data/app/models/spree/product.rb +1 -1
  9. data/app/models/spree/promotion/rules/option_value.rb +12 -3
  10. data/app/models/spree/promotion/rules/product.rb +22 -9
  11. data/app/models/spree/promotion/rules/taxon.rb +34 -26
  12. data/app/models/spree/promotion/rules/user.rb +17 -4
  13. data/app/models/spree/shipping_category.rb +6 -0
  14. data/app/models/spree/stock/quantifier.rb +1 -1
  15. data/app/models/spree/store.rb +56 -26
  16. data/app/models/spree/variant.rb +5 -1
  17. data/app/presenters/spree/variants/options_presenter.rb +1 -0
  18. data/app/services/spree/cart/remove_out_of_stock_items.rb +6 -12
  19. data/app/views/spree/addresses/_form.html.erb +4 -2
  20. data/lib/generators/spree/cursor_rules/templates/spree_rules.mdc +2 -2
  21. data/lib/generators/spree/dummy/dummy_generator.rb +34 -27
  22. data/lib/spree/core/ransack_configuration.rb +79 -0
  23. data/lib/spree/core/version.rb +1 -1
  24. data/lib/spree/core.rb +14 -0
  25. data/lib/spree/database_type_utilities.rb +4 -1
  26. data/lib/spree/testing_support/common_rake.rb +5 -2
  27. data/lib/spree/testing_support/factories/order_factory.rb +3 -0
  28. data/lib/spree/testing_support/factories/store_credit_factory.rb +1 -1
  29. data/lib/spree/testing_support/store.rb +6 -2
  30. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d349836816cd2c1ec5919e21868926cd4eec24cb0c38c33cb744b37a9402dc9
4
- data.tar.gz: 8072164578532d141040fe3aab664d91cb00c2c8571c3b9a043cd4ccb23f1aa1
3
+ metadata.gz: 4e0552e5475aa54e072158a70ea648d02d5514550eca293f1c0e0c79554c4216
4
+ data.tar.gz: afe49cf6d48fa819396d1518e3531435ffc22d038ce1a75aef430082bbc429fb
5
5
  SHA512:
6
- metadata.gz: 4ad9ed9392d8cb3b34cf3fa0c294dca288fe2c5162ed20c84616ef82c71e2368ea7debb5264208b26703df22640cb612e4deadb98882dcd872f8622197b8d0e5
7
- data.tar.gz: a6cd8afa6b70d0fc758758c9555ab24da37800c7905220b7d5f0251bf19fa06f87d03315aba858476eb181705c32ba6d80fba4e387f419f892f7b1ec16631aac
6
+ metadata.gz: 46b12717f12408f073b31a2a4f70d8850204bcfd59d32b20a6d891bc0326b7ea0c563c9b876e81156c6ba40f6ba88705553f87bc349598f52e8d761a7ea887a7
7
+ data.tar.gz: 37e34cbc4020055babe24354344aae7306dde8343267b2bac666fe691881936e5ebf6daeaa982ad4ce1ea4df88eecc77cb168c5321c57c2329b920dba37d05d7
@@ -1,6 +1,14 @@
1
1
  # https://github.com/spree-contrib/spree_address_book/blob/master/app/helpers/spree/addresses_helper.rb
2
2
  module Spree
3
3
  module AddressesHelper
4
+ def address_form_countries_states_cache_key
5
+ @address_form_countries_states_cache_key ||= [
6
+ I18n.locale,
7
+ current_store.cache_key_with_version,
8
+ current_store.checkout_zone&.cache_key_with_version
9
+ ].compact
10
+ end
11
+
4
12
  def address_field(form, method, address_id = 'b', required = false, text_field_attributes: {}, &handler)
5
13
  content_tag :div, id: [address_id, method].join, class: 'mb-4' do
6
14
  if handler
@@ -9,15 +9,18 @@ module Spree::RansackableAttributes
9
9
  self.default_ransackable_attributes = %w[id name updated_at created_at]
10
10
 
11
11
  def self.ransackable_associations(*_args)
12
- whitelisted_ransackable_associations || []
12
+ base = whitelisted_ransackable_associations || []
13
+ base | Spree.ransack.custom_associations_for(self)
13
14
  end
14
15
 
15
16
  def self.ransackable_attributes(*_args)
16
- default_ransackable_attributes | (whitelisted_ransackable_attributes || [])
17
+ base = default_ransackable_attributes | (whitelisted_ransackable_attributes || [])
18
+ base | Spree.ransack.custom_attributes_for(self)
17
19
  end
18
20
 
19
21
  def self.ransackable_scopes(*_args)
20
- whitelisted_ransackable_scopes || []
22
+ base = whitelisted_ransackable_scopes || []
23
+ base | Spree.ransack.custom_scopes_for(self)
21
24
  end
22
25
  end
23
26
  end
@@ -50,7 +50,7 @@ module Spree
50
50
  delegate :name, :description, :sku, :should_track_inventory?, :product, :options_text, :slug, :product_id, :dimensions_unit, :weight_unit, to: :variant
51
51
  delegate :brand, :category, to: :product
52
52
  delegate :tax_zone, to: :order
53
- delegate :digital?, to: :variant
53
+ delegate :digital?, :can_supply?, to: :variant
54
54
 
55
55
  scope :with_digital_assets, -> { joins(:variant).merge(Spree::Variant.with_digital_assets) }
56
56
 
@@ -134,7 +134,7 @@ module Spree
134
134
  alias money display_total
135
135
 
136
136
  def sufficient_stock?
137
- Spree::Stock::Quantifier.new(variant).can_supply? quantity
137
+ can_supply? quantity
138
138
  end
139
139
 
140
140
  def insufficient_stock?
@@ -5,10 +5,10 @@ module Spree
5
5
  #
6
6
  # @return [Boolean]
7
7
  def digital?
8
- if line_items.empty?
8
+ if item_count.zero? || line_items.empty?
9
9
  false
10
10
  else
11
- line_items.all?(&:digital?)
11
+ line_items.includes(variant: :product).all?(&:digital?)
12
12
  end
13
13
  end
14
14
 
@@ -16,21 +16,29 @@ module Spree
16
16
  #
17
17
  # @return [Boolean]
18
18
  def some_digital?
19
- line_items.any?(&:digital?)
19
+ if item_count.zero? || line_items.empty?
20
+ false
21
+ else
22
+ line_items.includes(variant: :product).any?(&:digital?)
23
+ end
20
24
  end
21
25
 
22
26
  # Returns true if any order line item has digital assets
23
27
  #
24
28
  # @return [Boolean]
25
29
  def with_digital_assets?
26
- line_items.any?(&:with_digital_assets?)
30
+ if item_count.zero? || line_items.empty?
31
+ false
32
+ else
33
+ line_items.includes(:variant).any?(&:with_digital_assets?)
34
+ end
27
35
  end
28
36
 
29
37
  # Returns all line items with digital assets
30
38
  #
31
39
  # @return [Array<Spree::LineItem>]
32
40
  def digital_line_items
33
- line_items.with_digital_assets.distinct
41
+ line_items.joins(:variant).with_digital_assets.distinct
34
42
  end
35
43
 
36
44
  # Returns all digital links for the order
@@ -63,6 +63,7 @@ module Spree
63
63
 
64
64
  def void_transaction!
65
65
  return true if void?
66
+ return void if response_code.blank?
66
67
 
67
68
  protect_from_connection_error do
68
69
  if payment_method.payment_profiles_supported?
@@ -39,6 +39,15 @@ module Spree
39
39
  #
40
40
  scope :with_body, -> { joins(:rich_text_body).distinct }
41
41
  scope :without_body, -> { where.missing(:rich_text_body) }
42
+ scope :with_matching_name, ->(name_to_match) do
43
+ value = name_to_match.to_s.strip.downcase
44
+
45
+ if Spree.use_translations?
46
+ i18n { name.lower.eq(value) }
47
+ else
48
+ where(arel_table[:name].lower.eq(value))
49
+ end
50
+ end
42
51
 
43
52
  #
44
53
  # Ransack
@@ -639,7 +639,7 @@ module Spree
639
639
  #
640
640
  # @return [Boolean]
641
641
  def digital?
642
- @digital ||= shipping_methods&.digital&.exists?
642
+ @digital ||= shipping_category&.includes_digital_shipping_method?
643
643
  end
644
644
 
645
645
  def auto_match_taxons
@@ -13,15 +13,24 @@ module Spree
13
13
  end
14
14
 
15
15
  def eligible?(promotable, _options = {})
16
+ return false if eligible_option_value_variant_ids.empty?
17
+
16
18
  case preferred_match_policy
17
19
  when 'any'
18
- promotable.line_items.any? { |item| actionable?(item) }
20
+ Spree::OptionValueVariant.where(id: eligible_option_value_variant_ids, variant_id: promotable.variant_ids).exists?
19
21
  end
20
22
  end
21
23
 
22
24
  def actionable?(line_item)
23
- option_value_variant_ids = line_item.variant.option_value_variant_ids.map(&:to_s)
24
- (preferred_eligible_values & option_value_variant_ids).any?
25
+ return false if eligible_option_value_variant_ids.empty?
26
+
27
+ Spree::OptionValueVariant.where(id: eligible_option_value_variant_ids, variant_id: line_item.variant_id).exists?
28
+ end
29
+
30
+ private
31
+
32
+ def eligible_option_value_variant_ids
33
+ @eligible_option_value_variant_ids ||= preferred_eligible_values.map(&:to_s)
25
34
  end
26
35
  end
27
36
  end
@@ -34,23 +34,27 @@ module Spree
34
34
  products
35
35
  end
36
36
 
37
+ def eligible_product_ids
38
+ @eligible_product_ids ||= product_promotion_rules.pluck(:product_id)
39
+ end
40
+
37
41
  def applicable?(promotable)
38
42
  promotable.is_a?(Spree::Order)
39
43
  end
40
44
 
41
45
  def eligible?(order, _options = {})
42
- return true if eligible_products.empty?
46
+ return true if eligible_product_ids.empty?
43
47
 
44
48
  if preferred_match_policy == 'all'
45
- unless eligible_products.all? { |p| order.products.include?(p) }
49
+ unless eligible_product_ids.all? { |p| order.product_ids.include?(p) }
46
50
  eligibility_errors.add(:base, eligibility_error_message(:missing_product))
47
51
  end
48
52
  elsif preferred_match_policy == 'any'
49
- unless order.products.any? { |p| eligible_products.include?(p) }
53
+ unless order.product_ids.any? { |p| eligible_product_ids.include?(p) }
50
54
  eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products))
51
55
  end
52
56
  else
53
- unless order.products.none? { |p| eligible_products.include?(p) }
57
+ unless order.product_ids.none? { |p| eligible_product_ids.include?(p) }
54
58
  eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product))
55
59
  end
56
60
  end
@@ -61,9 +65,9 @@ module Spree
61
65
  def actionable?(line_item)
62
66
  case preferred_match_policy
63
67
  when 'any', 'all'
64
- product_ids.include? line_item.variant.product_id
68
+ eligible_product_ids.include? line_item.variant.product_id
65
69
  when 'none'
66
- product_ids.exclude? line_item.variant.product_id
70
+ eligible_product_ids.exclude? line_item.variant.product_id
67
71
  else
68
72
  raise "unexpected match policy: #{preferred_match_policy.inspect}"
69
73
  end
@@ -89,9 +93,18 @@ module Spree
89
93
  return if product_ids_to_add.nil?
90
94
 
91
95
  product_promotion_rules.delete_all
92
- product_promotion_rules.insert_all(
93
- product_ids_to_add.map { |product_id| { product_id: product_id, promotion_rule_id: id } }
94
- )
96
+
97
+ if product_ids_to_add.any?
98
+ Spree::ProductPromotionRule.insert_all(
99
+ product_ids_to_add.map { |product_id| { product_id: product_id, promotion_rule_id: id } }
100
+ )
101
+ end
102
+
103
+ # Invalidate cache after bulk operations
104
+ touch
105
+
106
+ # Clear memoized values
107
+ @eligible_product_ids = nil
95
108
  end
96
109
  end
97
110
  end
@@ -30,14 +30,21 @@ module Spree
30
30
  promotable.is_a?(Spree::Order)
31
31
  end
32
32
 
33
+ def eligible_taxon_ids
34
+ @eligible_taxon_ids ||= promotion_rule_taxons.pluck(:taxon_id)
35
+ end
36
+
33
37
  def eligible?(order, _options = {})
38
+ return true if eligible_taxon_ids.empty?
39
+
34
40
  if preferred_match_policy == 'all'
35
- unless (taxons.to_a - taxons_in_order_including_parents(order)).empty?
41
+ order_taxon_ids_with_ancestors = taxon_ids_in_order_including_ancestors(order)
42
+ unless eligible_taxon_ids.all? { |id| order_taxon_ids_with_ancestors.include?(id) }
36
43
  eligibility_errors.add(:base, eligibility_error_message(:missing_taxon))
37
44
  end
38
45
  else
39
- order_taxons = taxons_in_order_including_parents(order)
40
- unless taxons.any? { |taxon| order_taxons.include? taxon }
46
+ order_taxon_ids_with_ancestors = taxon_ids_in_order_including_ancestors(order)
47
+ unless eligible_taxon_ids.any? { |id| order_taxon_ids_with_ancestors.include?(id) }
41
48
  eligibility_errors.add(:base, eligibility_error_message(:no_matching_taxons))
42
49
  end
43
50
  end
@@ -46,12 +53,7 @@ module Spree
46
53
  end
47
54
 
48
55
  def actionable?(line_item)
49
- store = line_item.order.store
50
-
51
- store.products.
52
- joins(:classifications).
53
- where(Spree::Classification.table_name => { taxon_id: taxon_ids, product_id: line_item.product_id }).
54
- exists?
56
+ Spree::Classification.where(taxon_id: eligible_taxon_ids_including_children, product_id: line_item.product_id).exists?
55
57
  end
56
58
 
57
59
  def taxon_ids_string
@@ -71,34 +73,40 @@ module Spree
71
73
 
72
74
  private
73
75
 
74
- # All taxons in an order
75
- def order_taxons(order)
76
- taxon_ids = Spree::Classification.where(product_id: order.product_ids).pluck(:taxon_id).uniq
76
+ # IDs of taxons in rule including all their children
77
+ def eligible_taxon_ids_including_children
78
+ @eligible_taxon_ids_including_children ||= begin
79
+ return [] if eligible_taxon_ids.empty?
77
80
 
78
- order.store.taxons.where(id: taxon_ids)
81
+ Spree::Taxon.where(id: eligible_taxon_ids).flat_map(&:cached_self_and_descendants_ids).uniq
82
+ end
79
83
  end
80
84
 
81
- # ids of taxons rules and taxons rules children
82
- def taxons_including_children_ids
83
- taxons.inject([]) { |ids, taxon| ids += taxon.self_and_descendants.ids }
84
- end
85
+ # IDs of taxons in order that match rule taxons (or their children), plus all ancestors
86
+ def taxon_ids_in_order_including_ancestors(order)
87
+ # Get taxon IDs from order products that are within rule taxons or their children
88
+ order_taxon_ids = Spree::Classification.where(product_id: order.product_ids, taxon_id: eligible_taxon_ids_including_children).pluck(:taxon_id).uniq
85
89
 
86
- # taxons order vs taxons rules and taxons rules children
87
- def order_taxons_in_taxons_and_children(order)
88
- order_taxons(order).where(id: taxons_including_children_ids)
89
- end
90
+ return [] if order_taxon_ids.empty?
90
91
 
91
- def taxons_in_order_including_parents(order)
92
- order_taxons_in_taxons_and_children(order).inject([]) { |taxons, taxon| taxons << taxon.self_and_ancestors }.flatten.uniq
92
+ # Get those taxons plus all their ancestors
93
+ Spree::Taxon.where(id: order_taxon_ids).flat_map { |taxon| taxon.self_and_ancestors.ids }.uniq
93
94
  end
94
95
 
95
96
  def add_taxons
96
97
  return if taxon_ids_to_add.nil?
97
98
 
98
99
  promotion_rule_taxons.delete_all
99
- promotion_rule_taxons.insert_all(
100
- taxon_ids_to_add.map { |taxon_id| { taxon_id: taxon_id, promotion_rule_id: id } }
101
- )
100
+
101
+ if taxon_ids_to_add.any?
102
+ Spree::PromotionRuleTaxon.insert_all(
103
+ taxon_ids_to_add.map { |taxon_id| { taxon_id: taxon_id, promotion_rule_id: id } }
104
+ )
105
+ end
106
+
107
+ # Clear memoized values
108
+ @eligible_taxon_ids = nil
109
+ @eligible_taxon_ids_including_children = nil
102
110
  end
103
111
  end
104
112
  end
@@ -24,8 +24,12 @@ module Spree
24
24
  promotable.is_a?(Spree::Order)
25
25
  end
26
26
 
27
+ def eligible_user_ids
28
+ @eligible_user_ids ||= promotion_rule_users.pluck(:user_id)
29
+ end
30
+
27
31
  def eligible?(order, _options = {})
28
- user_ids.include?(order.user_id)
32
+ eligible_user_ids.include?(order.user_id)
29
33
  end
30
34
 
31
35
  def user_ids_string
@@ -50,9 +54,18 @@ module Spree
50
54
  return if user_ids_to_add.nil?
51
55
 
52
56
  promotion_rule_users.delete_all
53
- promotion_rule_users.insert_all(
54
- user_ids_to_add.map { |user_id| { user_id: user_id, promotion_rule_id: id } }
55
- )
57
+
58
+ if user_ids_to_add.any?
59
+ Spree::PromotionRuleUser.insert_all(
60
+ user_ids_to_add.map { |user_id| { user_id: user_id, promotion_rule_id: id } }
61
+ )
62
+ end
63
+
64
+ # Invalidate cache after bulk operations
65
+ touch
66
+
67
+ # Clear memoized values
68
+ @eligible_user_ids = nil
56
69
  end
57
70
  end
58
71
  end
@@ -16,5 +16,11 @@ module Spree
16
16
  def self.digital
17
17
  find_by(name: DIGITAL_NAME)
18
18
  end
19
+
20
+ def includes_digital_shipping_method?
21
+ Rails.cache.fetch("#{cache_key_with_version}/includes-digital-shipping-method") do
22
+ shipping_methods.digital.exists?
23
+ end
24
+ end
19
25
  end
20
26
  end
@@ -25,7 +25,7 @@ module Spree
25
25
  end
26
26
 
27
27
  def can_supply?(required = 1)
28
- variant.available? && (total_on_hand >= required || backorderable?)
28
+ variant.available? && (backorderable? || total_on_hand >= required)
29
29
  end
30
30
 
31
31
  def stock_items
@@ -496,48 +496,78 @@ module Spree
496
496
  end
497
497
 
498
498
  def ensure_default_taxonomies_are_created
499
- taxonomies.find_or_create_by(name: I18n.t('spree.taxonomy_categories_name', default: I18n.t('spree.taxonomy_categories_name', locale: :en)))
500
- taxonomies.find_or_create_by(name: I18n.t('spree.taxonomy_brands_name', default: I18n.t('spree.taxonomy_brands_name', locale: :en)))
501
- taxonomies.find_or_create_by(name: I18n.t('spree.taxonomy_collections_name', default: I18n.t('spree.taxonomy_collections_name', locale: :en)))
502
- rescue ActiveRecord::NotNullViolation
499
+ [
500
+ translate_with_store_locale_fallback('spree.taxonomy_categories_name'),
501
+ translate_with_store_locale_fallback('spree.taxonomy_brands_name'),
502
+ translate_with_store_locale_fallback('spree.taxonomy_collections_name')
503
+ ].each do |taxonomy_name|
504
+ # Manual exists?/create to work around Mobility bug with find_or_create_by
505
+ next if taxonomies.with_matching_name(taxonomy_name).exists?
506
+
507
+ taxonomies.create(name: taxonomy_name)
508
+ end
503
509
  end
504
510
 
505
511
  def ensure_default_automatic_taxons
506
- collections_taxonomy = taxonomies.find_by(name: Spree.t(:taxonomy_collections_name))
512
+ # Use Mobility-safe lookup for taxonomy
513
+ collections_taxonomy = taxonomies.with_matching_name(translate_with_store_locale_fallback('spree.taxonomy_collections_name')).first
514
+ return unless collections_taxonomy.present?
507
515
 
508
- if collections_taxonomy.present?
509
- on_sale_taxon = collections_taxonomy.taxons.automatic.where(name: Spree.t('automatic_taxon_names.on_sale')).first_or_create! do |taxon|
510
- taxon.parent = collections_taxonomy.root
511
- taxon.rules.new(type: 'Spree::TaxonRules::Sale', value: 'true')
512
- end
516
+ automatic_taxons_config = [
517
+ { name: translate_with_store_locale_fallback('spree.automatic_taxon_names.on_sale'), rule_type: 'Spree::TaxonRules::Sale', rule_value: 'true' },
518
+ { name: translate_with_store_locale_fallback('spree.automatic_taxon_names.new_arrivals'), rule_type: 'Spree::TaxonRules::AvailableOn', rule_value: 30 }
519
+ ]
513
520
 
514
- new_arrivals_taxon = collections_taxonomy.taxons.automatic.where(name: Spree.t('automatic_taxon_names.new_arrivals')).first_or_create! do |taxon|
515
- taxon.parent = collections_taxonomy.root
516
- taxon.rules.new(type: 'Spree::TaxonRules::AvailableOn', value: 30)
517
- end
521
+ automatic_taxons_config.map do |config|
522
+ # Manual exists?/create to work around Mobility bug with first_or_create
523
+ taxon_scope = collections_taxonomy.taxons.automatic.with_matching_name(config[:name])
518
524
 
519
- [on_sale_taxon, new_arrivals_taxon]
525
+ if taxon_scope.exists?
526
+ taxon_scope.first
527
+ else
528
+ collections_taxonomy.taxons.create!(
529
+ name: config[:name],
530
+ automatic: true,
531
+ parent: collections_taxonomy.root,
532
+ taxon_rules: [TaxonRule.new(type: config[:rule_type], value: config[:rule_value])]
533
+ )
534
+ end
520
535
  end
521
536
  end
522
537
 
523
538
  def ensure_default_post_categories_are_created
524
- post_categories.find_or_create_by(title: Spree.t('default_post_categories.resources'))
525
- post_categories.find_or_create_by(title: Spree.t('default_post_categories.articles'))
526
- post_categories.find_or_create_by(title: Spree.t('default_post_categories.news'))
539
+ [
540
+ translate_with_store_locale_fallback('spree.default_post_categories.resources'),
541
+ translate_with_store_locale_fallback('spree.default_post_categories.articles'),
542
+ translate_with_store_locale_fallback('spree.default_post_categories.news')
543
+ ].each do |category_title|
544
+ # Use exists?/create pattern for safety
545
+ next if post_categories.where(title: category_title).exists?
546
+
547
+ post_categories.create(title: category_title)
548
+ end
527
549
  end
528
550
 
529
551
  def create_default_policies
530
- policies.find_or_create_by(name: Spree.t('terms_of_service'))
531
- policies.find_or_create_by(name: Spree.t('privacy_policy'))
532
- policies.find_or_create_by(name: Spree.t('returns_policy'))
533
- policies.find_or_create_by(name: Spree.t('shipping_policy'))
534
-
535
- # Create checkout links to the policies
536
- policies.each do |policy|
537
- links.find_or_create_by(linkable: policy)
552
+ [
553
+ translate_with_store_locale_fallback('spree.terms_of_service'),
554
+ translate_with_store_locale_fallback('spree.privacy_policy'),
555
+ translate_with_store_locale_fallback('spree.returns_policy'),
556
+ translate_with_store_locale_fallback('spree.shipping_policy')
557
+ ].each do |policy_name|
558
+ # Manual exists?/create to work around Mobility bug with find_or_create_by
559
+ next if policies.with_matching_name(policy_name).exists?
560
+
561
+ policies.create(name: policy_name)
538
562
  end
539
563
  end
540
564
 
565
+ # Translates a key using the store's default locale with fallback to :en
566
+ def translate_with_store_locale_fallback(key)
567
+ locale = default_locale.presence&.to_sym || :en
568
+ I18n.t(key, locale: locale, default: I18n.t(key, locale: :en))
569
+ end
570
+
541
571
  # code is slug, so we don't want to generate new slug when code changes
542
572
  # we use friendlyId only for history feature
543
573
  def should_generate_new_friendly_id?
@@ -245,7 +245,11 @@ module Spree
245
245
  end
246
246
 
247
247
  def options_text
248
- @options_text ||= Spree::Variants::OptionsPresenter.new(self).to_sentence
248
+ @options_text ||= if option_values.loaded?
249
+ option_values.sort_by { |ov| ov.option_type.position }.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
250
+ else
251
+ option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
252
+ end
249
253
  end
250
254
 
251
255
  # Default to master name
@@ -8,6 +8,7 @@ module Spree
8
8
  delegate :option_values, to: :variant
9
9
 
10
10
  def initialize(variant)
11
+ Spree::Deprecation.warn('Spree::Variants::OptionsPresenter is deprecated and will be removed in Spree 5.5. Please use Spree::Variant#options_text instead.')
11
12
  @variant = variant
12
13
  end
13
14
 
@@ -5,8 +5,13 @@ module Spree
5
5
 
6
6
  def call(order:)
7
7
  @messages = []
8
+
9
+ return success([order, @messages]) if order.item_count.zero? || order.line_items.none?
10
+
11
+ line_items = order.line_items.includes(variant: [:product, :stock_items, :stock_locations, { stock_items: :stock_location }])
12
+
8
13
  ActiveRecord::Base.transaction do
9
- line_items(order).each do |line_item|
14
+ line_items.each do |line_item|
10
15
  cart_remove_line_item_service.call(order: order, line_item: line_item) if !valid_status?(line_item) || !stock_available?(line_item)
11
16
  end
12
17
  end
@@ -20,17 +25,6 @@ module Spree
20
25
 
21
26
  private
22
27
 
23
- def line_items(order)
24
- if order.line_items.empty?
25
- []
26
- elsif order.line_items.first.association_cached?(:variant) && order.line_items.first.variant.association_cached?(:product)
27
- # Don't include associations if it is already included, because it breaks other includes
28
- order.line_items
29
- else
30
- order.line_items.includes(variant: :product)
31
- end
32
- end
33
-
34
28
  def valid_status?(line_item)
35
29
  product = line_item.product
36
30
  if !product.active? || product.deleted? || product.discontinued? || line_item.variant.discontinued?
@@ -3,8 +3,10 @@
3
3
 
4
4
  <div class="inner"
5
5
  data-controller="address-form address-autocomplete"
6
- data-address-form-countries-value="<%= available_countries.to_json %>"
7
- data-address-form-states-value="<%= available_states.to_json %>"
6
+ <% cache address_form_countries_states_cache_key do %>
7
+ data-address-form-countries-value="<%= available_countries.to_json %>"
8
+ data-address-form-states-value="<%= available_states.to_json %>"
9
+ <% end %>
8
10
  data-address-form-current-state-id-value="<%= address.state_id || address_form.object.state_id %>"
9
11
  >
10
12
  <div id="<%= "#{address_id}country" %>">
@@ -148,7 +148,7 @@ class MyAddToCartService < Spree::Cart::AddItem
148
148
  def call(order:, variant:, quantity: nil, public_metadata: {}, private_metadata: {}, options: {})
149
149
  ApplicationRecord.transaction do
150
150
  run :add_to_line_item
151
- run Spree::Dependencies.cart_recalculate_service.constantize
151
+ run Spree.cart_recalculate_service
152
152
  run :update_external_system
153
153
  end
154
154
  end
@@ -363,7 +363,7 @@ module Spree
363
363
  private
364
364
 
365
365
  def serialized_collection
366
- Spree::Api::Dependencies.storefront_product_serializer.constantize.new(
366
+ Spree.api.storefront_product_serializer.new(
367
367
  collection,
368
368
  include: resource_includes,
369
369
  fields: sparse_fields
@@ -10,6 +10,7 @@ module Spree
10
10
 
11
11
  class_option :lib_name, default: ''
12
12
  class_option :database, default: ''
13
+ class_option :api, type: :boolean, default: false
13
14
 
14
15
  def self.source_paths
15
16
  paths = superclass.source_paths
@@ -21,34 +22,40 @@ module Spree
21
22
  remove_directory_if_exists(dummy_path)
22
23
  end
23
24
 
24
- PASSTHROUGH_OPTIONS = [
25
- :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip
26
- ]
27
-
28
25
  def generate_test_dummy
29
- # calling slice on a Thor::CoreExtensions::HashWithIndifferentAccess
30
- # object has been known to return nil
31
- opts = {}.merge(options).slice(*PASSTHROUGH_OPTIONS)
32
- opts[:database] = 'sqlite3' if opts[:database].blank?
33
- opts[:force] = true
34
- opts[:skip_bundle] = true
35
- opts[:skip_git] = true
36
- opts[:skip_listen] = true
37
- opts[:skip_rc] = true
38
- opts[:skip_spring] = true
39
- opts[:skip_test] = true
40
- opts[:skip_bootsnap] = true
41
- opts[:skip_docker] = true
42
- opts[:skip_rubocop] = true
43
- opts[:skip_brakeman] = true
44
- opts[:skip_ci] = true
45
- opts[:skip_kamal] = true
46
- opts[:skip_devcontainer] = true
47
- opts[:skip_solid] = true
48
-
49
26
  puts 'Generating dummy Rails application...'
50
- invoke Rails::Generators::AppGenerator,
51
- [File.expand_path(dummy_path, destination_root)], opts
27
+
28
+ args = [File.expand_path(dummy_path, destination_root)]
29
+
30
+ # Database
31
+ args << "--database=#{options[:database].presence || 'sqlite3'}"
32
+
33
+ # Skip options
34
+ args << '--force'
35
+ args << '--skip-bundle'
36
+ args << '--skip-git'
37
+ args << '--skip-keeps'
38
+ args << '--skip-rc'
39
+ args << '--skip-spring'
40
+ args << '--skip-test'
41
+ args << '--skip-bootsnap'
42
+ args << '--skip-docker'
43
+ args << '--skip-rubocop'
44
+ args << '--skip-brakeman'
45
+ args << '--skip-ci'
46
+ args << '--skip-kamal'
47
+ args << '--skip-devcontainer'
48
+ args << '--skip-solid'
49
+ args << '--skip-thruster'
50
+ args << '--skip-bundler-audit'
51
+ args << '--skip-dev-gems'
52
+ args << '--skip-action-mailbox'
53
+ args << '--skip-jbuilder'
54
+
55
+ # API mode (implies skip-asset-pipeline, skip-javascript, skip-hotwire)
56
+ args << '--api' if options[:api]
57
+
58
+ Rails::Generators.invoke('app', args)
52
59
  inject_yaml_permitted_classes
53
60
  end
54
61
 
@@ -62,7 +69,7 @@ module Spree
62
69
  template 'rails/routes.rb', "#{dummy_path}/config/routes.rb", force: true
63
70
  template 'rails/test.rb', "#{dummy_path}/config/environments/test.rb", force: true
64
71
  template 'initializers/devise.rb', "#{dummy_path}/config/initializers/devise.rb", force: true
65
- template "app/assets/config/manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true
72
+ template "app/assets/config/manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true unless options[:api]
66
73
  end
67
74
 
68
75
  def test_dummy_inject_extension_requirements
@@ -0,0 +1,79 @@
1
+ module Spree
2
+ # Centralized configuration for Ransack searchable attributes, associations, and scopes.
3
+ #
4
+ # This class allows developers to extend Spree models with custom ransackable
5
+ # configurations without using decorators.
6
+ #
7
+ # @example Adding custom searchable fields
8
+ # Spree.ransack.add_attribute(Spree::Product, :vendor_id)
9
+ # Spree.ransack.add_scope(Spree::Product, :by_vendor)
10
+ # Spree.ransack.add_association(Spree::Product, :vendor)
11
+ #
12
+ class RansackConfiguration
13
+ def initialize
14
+ @custom_attributes = Hash.new { |h, k| h[k] = [] }
15
+ @custom_associations = Hash.new { |h, k| h[k] = [] }
16
+ @custom_scopes = Hash.new { |h, k| h[k] = [] }
17
+ end
18
+
19
+ # Add a custom ransackable attribute to a model.
20
+ #
21
+ # @param model [Class] the model class to configure (e.g., Spree::Product)
22
+ # @param attribute [String, Symbol] the attribute to add
23
+ # @return [Array<String>] the updated list of custom attributes
24
+ def add_attribute(model, attribute)
25
+ @custom_attributes[model.name.to_sym] |= [attribute.to_s]
26
+ end
27
+
28
+ # Add a custom ransackable association to a model.
29
+ #
30
+ # @param model [Class] the model class to configure (e.g., Spree::Product)
31
+ # @param association [String, Symbol] the association to add
32
+ # @return [Array<String>] the updated list of custom associations
33
+ def add_association(model, association)
34
+ @custom_associations[model.name.to_sym] |= [association.to_s]
35
+ end
36
+
37
+ # Add a custom ransackable scope to a model.
38
+ #
39
+ # @param model [Class] the model class to configure (e.g., Spree::Product)
40
+ # @param scope [String, Symbol] the scope to add
41
+ # @return [Array<String>] the updated list of custom scopes
42
+ def add_scope(model, scope)
43
+ @custom_scopes[model.name.to_sym] |= [scope.to_s]
44
+ end
45
+
46
+ # Get custom ransackable attributes for a model.
47
+ #
48
+ # @param model [Class] the model class to query
49
+ # @return [Array<String>] the custom attributes
50
+ def custom_attributes_for(model)
51
+ @custom_attributes[model.name.to_sym]
52
+ end
53
+
54
+ # Get custom ransackable associations for a model.
55
+ #
56
+ # @param model [Class] the model class to query
57
+ # @return [Array<String>] the custom associations
58
+ def custom_associations_for(model)
59
+ @custom_associations[model.name.to_sym]
60
+ end
61
+
62
+ # Get custom ransackable scopes for a model.
63
+ #
64
+ # @param model [Class] the model class to query
65
+ # @return [Array<String>] the custom scopes
66
+ def custom_scopes_for(model)
67
+ @custom_scopes[model.name.to_sym]
68
+ end
69
+
70
+ # Reset all custom configurations. Useful for testing.
71
+ #
72
+ # @return [void]
73
+ def reset!
74
+ @custom_attributes.clear
75
+ @custom_associations.clear
76
+ @custom_scopes.clear
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.2.5'.freeze
2
+ VERSION = '5.2.6'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/spree/core.rb CHANGED
@@ -374,6 +374,19 @@ module Spree
374
374
  @permissions ||= PermissionConfiguration.new
375
375
  end
376
376
 
377
+ # Ransack configuration accessor for managing custom ransackable attributes,
378
+ # associations, and scopes across Spree models.
379
+ #
380
+ # @example Adding custom searchable fields
381
+ # Spree.ransack.add_attribute(Spree::Product, :vendor_id)
382
+ # Spree.ransack.add_scope(Spree::Product, :by_vendor)
383
+ # Spree.ransack.add_association(Spree::Product, :vendor)
384
+ #
385
+ # @return [Spree::RansackConfiguration] the ransack configuration instance
386
+ def self.ransack
387
+ @ransack ||= RansackConfiguration.new
388
+ end
389
+
377
390
  class << self
378
391
  # Dynamic methods for core dependencies
379
392
  #
@@ -451,3 +464,4 @@ require 'spree/core/preferences/runtime_configuration'
451
464
 
452
465
  require 'spree/core/webhooks'
453
466
  require 'spree/core/permission_configuration'
467
+ require 'spree/core/ransack_configuration'
@@ -1,9 +1,12 @@
1
1
  module Spree
2
2
  module DatabaseTypeUtilities
3
+ # Maximum value for a 4-byte signed integer (default database integer type)
4
+ INTEGER_MAX = (2**31) - 1
5
+
3
6
  def self.maximum_value_for(data_type)
4
7
  case data_type
5
8
  when :integer
6
- ActiveModel::Type::Integer.new.instance_eval { range.max }
9
+ INTEGER_MAX
7
10
  else
8
11
  raise ArgumentError, 'Currently only :integer argument is acceptable'
9
12
  end
@@ -19,12 +19,15 @@ namespace :common do
19
19
  ENV['RAILS_ENV'] = 'test'
20
20
  Rails.env = 'test'
21
21
 
22
- skip_javascript = ['spree/api', 'spree/core', 'spree/sample', 'spree/emails'].include?(ENV['LIB_NAME'])
22
+ api_only = ['spree/api', 'spree/core', 'spree/sample'].include?(ENV['LIB_NAME'])
23
+ skip_javascript = api_only || ENV['LIB_NAME'] == 'spree/emails'
23
24
 
24
25
  dummy_app_args = [
25
26
  "--lib_name=#{ENV['LIB_NAME']}"
26
27
  ]
27
- if skip_javascript
28
+ if api_only
29
+ dummy_app_args << '--api'
30
+ elsif skip_javascript
28
31
  dummy_app_args << '--skip_javascript'
29
32
  end
30
33
  Spree::DummyGenerator.start dummy_app_args
@@ -24,6 +24,9 @@ FactoryBot.define do
24
24
  after(:create) do |order, evaluator|
25
25
  create(:line_item, order: order, price: evaluator.line_items_price)
26
26
  order.line_items.reload # to ensure order.line_items is accessible after
27
+
28
+ order.update_column(:item_count, order.line_items.count)
29
+ order.reload
27
30
  end
28
31
  end
29
32
 
@@ -3,7 +3,7 @@ FactoryBot.define do
3
3
 
4
4
  factory :store_credit, class: Spree::StoreCredit do
5
5
  user
6
- created_by { create(:user) }
6
+ created_by { create(:admin_user) }
7
7
  category { create(:store_credit_category) }
8
8
  amount { 150.00 }
9
9
  currency { 'USD' }
@@ -13,8 +13,12 @@ RSpec.configure do |config|
13
13
 
14
14
  config.before(:all) do
15
15
  unless self.class.metadata[:without_global_store]
16
- @default_country = Spree::Country.find_by(iso: 'US') || FactoryBot.create(:country_us)
17
- @default_store = Spree::Store.find_by(default: true) || FactoryBot.create(:store, default: true, default_country: @default_country, default_currency: 'USD')
16
+ # Ensure locale is set to :en before creating store to avoid translation issues
17
+ # when previous tests left the locale in a different language
18
+ I18n.with_locale(:en) do
19
+ @default_country = Spree::Country.find_by(iso: 'US') || FactoryBot.create(:country_us)
20
+ @default_store = Spree::Store.find_by(default: true) || FactoryBot.create(:store, default: true, default_country: @default_country, default_currency: 'USD')
21
+ end
18
22
  end
19
23
  end
20
24
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.5
4
+ version: 5.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -34,7 +34,7 @@ dependencies:
34
34
  version: '7.2'
35
35
  - - "<"
36
36
  - !ruby/object:Gem::Version
37
- version: '8.1'
37
+ version: '8.2'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -44,7 +44,7 @@ dependencies:
44
44
  version: '7.2'
45
45
  - - "<"
46
46
  - !ruby/object:Gem::Version
47
- version: '8.1'
47
+ version: '8.2'
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: activemerchant
50
50
  requirement: !ruby/object:Gem::Requirement
@@ -1549,6 +1549,7 @@ files:
1549
1549
  - lib/spree/core/query_filters/date.rb
1550
1550
  - lib/spree/core/query_filters/number.rb
1551
1551
  - lib/spree/core/query_filters/text.rb
1552
+ - lib/spree/core/ransack_configuration.rb
1552
1553
  - lib/spree/core/routes.rb
1553
1554
  - lib/spree/core/search/base.rb
1554
1555
  - lib/spree/core/token_generator.rb
@@ -1703,9 +1704,9 @@ licenses:
1703
1704
  - BSD-3-Clause
1704
1705
  metadata:
1705
1706
  bug_tracker_uri: https://github.com/spree/spree/issues
1706
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.2.5
1707
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.2.6
1707
1708
  documentation_uri: https://docs.spreecommerce.org/
1708
- source_code_uri: https://github.com/spree/spree/tree/v5.2.5
1709
+ source_code_uri: https://github.com/spree/spree/tree/v5.2.6
1709
1710
  rdoc_options: []
1710
1711
  require_paths:
1711
1712
  - lib