spree_core 5.5.3 → 5.6.0.rc1

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +12 -2
  3. data/app/helpers/spree/currency_helper.rb +1 -4
  4. data/app/helpers/spree/locale_helper.rb +18 -13
  5. data/app/helpers/spree/localized_names_helper.rb +25 -0
  6. data/app/mailers/spree/admin_user_mailer.rb +71 -0
  7. data/app/mailers/spree/base_mailer.rb +73 -11
  8. data/app/mailers/spree/export_mailer.rb +7 -7
  9. data/app/mailers/spree/import_mailer.rb +19 -0
  10. data/app/mailers/spree/invitation_mailer.rb +16 -11
  11. data/app/mailers/spree/report_mailer.rb +6 -6
  12. data/app/mailers/spree/webhook_mailer.rb +7 -6
  13. data/app/models/concerns/spree/admin_user_methods.rb +44 -0
  14. data/app/models/concerns/spree/legacy_multi_store_support.rb +48 -0
  15. data/app/models/concerns/spree/presentation_translatable.rb +39 -0
  16. data/app/models/concerns/spree/product_scopes.rb +21 -3
  17. data/app/models/concerns/spree/single_store_resource.rb +5 -0
  18. data/app/models/concerns/spree/store_scoped_resource.rb +10 -0
  19. data/app/models/concerns/spree/stores/markets.rb +9 -1
  20. data/app/models/concerns/spree/stores/setup.rb +37 -23
  21. data/app/models/concerns/spree/translatable_resource.rb +95 -5
  22. data/app/models/concerns/spree/user_methods.rb +27 -2
  23. data/app/models/spree/ability.rb +5 -4
  24. data/app/models/spree/allowed_origin.rb +31 -0
  25. data/app/models/spree/category.rb +12 -2
  26. data/app/models/spree/channel/gating.rb +67 -0
  27. data/app/models/spree/channel.rb +1 -0
  28. data/app/models/spree/classification.rb +12 -0
  29. data/app/models/spree/country.rb +22 -1
  30. data/app/models/spree/credit_card.rb +29 -0
  31. data/app/models/spree/currency.rb +47 -0
  32. data/app/models/spree/current.rb +12 -1
  33. data/app/models/spree/customer_group.rb +1 -0
  34. data/app/models/spree/export.rb +19 -0
  35. data/app/models/spree/exports/coupon_codes.rb +1 -2
  36. data/app/models/spree/import.rb +84 -1
  37. data/app/models/spree/import_row.rb +7 -1
  38. data/app/models/spree/imports/product_translations.rb +6 -0
  39. data/app/models/spree/locale.rb +104 -0
  40. data/app/models/spree/option_type.rb +11 -13
  41. data/app/models/spree/option_value.rb +5 -17
  42. data/app/models/spree/order.rb +19 -2
  43. data/app/models/spree/payment_method.rb +6 -5
  44. data/app/models/spree/policy.rb +2 -1
  45. data/app/models/spree/product.rb +23 -2
  46. data/app/models/spree/promotion/rules/channel.rb +37 -0
  47. data/app/models/spree/promotion/rules/market.rb +37 -0
  48. data/app/models/spree/promotion.rb +5 -3
  49. data/app/models/spree/promotion_handler/promotion_duplicator.rb +1 -1
  50. data/app/models/spree/role_user.rb +4 -0
  51. data/app/models/spree/search_provider/meilisearch.rb +22 -9
  52. data/app/models/spree/setup_task.rb +12 -0
  53. data/app/models/spree/setup_tasks.rb +86 -0
  54. data/app/models/spree/shipment.rb +38 -1
  55. data/app/models/spree/shipment_handler.rb +1 -1
  56. data/app/models/spree/stock/availability_validator.rb +1 -1
  57. data/app/models/spree/stock/quantifier.rb +21 -1
  58. data/app/models/spree/stock_item.rb +2 -1
  59. data/app/models/spree/stock_location.rb +4 -1
  60. data/app/models/spree/store.rb +45 -9
  61. data/app/models/spree/store_payment_method.rb +5 -1
  62. data/app/models/spree/store_promotion.rb +4 -0
  63. data/app/models/spree/taxon.rb +112 -9
  64. data/app/models/spree/taxonomy.rb +1 -1
  65. data/app/models/spree/translations/batch.rb +104 -0
  66. data/app/models/spree/variant.rb +29 -2
  67. data/app/presenters/spree/search_provider/product_presenter.rb +3 -0
  68. data/app/services/spree/carts/complete.rb +3 -0
  69. data/app/services/spree/fulfillments/create.rb +259 -0
  70. data/app/services/spree/locales/set_fallback_locale_for_store.rb +7 -1
  71. data/app/services/spree/locales.rb +52 -0
  72. data/app/services/spree/products/prepare_nested_attributes.rb +40 -12
  73. data/app/services/spree/shipments/update.rb +1 -1
  74. data/app/services/spree/stock_reservations/reserve.rb +4 -1
  75. data/app/services/spree/taxons/add_products.rb +3 -0
  76. data/app/services/spree/taxons/remove_products.rb +3 -0
  77. data/app/subscribers/spree/admin_user_email_subscriber.rb +29 -0
  78. data/app/subscribers/spree/import_email_subscriber.rb +26 -0
  79. data/app/views/layouts/spree/base_mailer.html.erb +14 -17
  80. data/app/views/spree/admin_user_mailer/confirmation_email.html.erb +7 -0
  81. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +8 -0
  82. data/app/views/spree/export_mailer/export_done.html.erb +6 -14
  83. data/app/views/spree/import_mailer/import_done.html.erb +12 -0
  84. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +4 -12
  85. data/app/views/spree/invitation_mailer/invitation_email.html.erb +11 -24
  86. data/app/views/spree/report_mailer/report_done.html.erb +6 -14
  87. data/app/views/spree/shared/_base_mailer_footer.html.erb +15 -8
  88. data/app/views/spree/shared/_base_mailer_header.html.erb +7 -5
  89. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +211 -273
  90. data/app/views/spree/shared/_mailer_button.html.erb +10 -0
  91. data/app/views/spree/shared/_mailer_hero.html.erb +12 -0
  92. data/app/views/spree/shared/_purchased_items_styles.html.erb +112 -0
  93. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +39 -14
  94. data/config/initializers/spree_store_setup_tasks.rb +23 -0
  95. data/config/locales/en.yml +42 -992
  96. data/db/migrate/20260613000001_add_store_id_to_spree_role_users.rb +10 -0
  97. data/db/migrate/20260626000001_add_store_id_to_spree_taxons.rb +11 -0
  98. data/db/migrate/20260627000001_add_products_count_to_spree_taxons.rb +6 -0
  99. data/db/migrate/20260628000001_add_store_id_to_spree_promotions.rb +10 -0
  100. data/db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb +11 -0
  101. data/db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb +7 -0
  102. data/db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb +39 -0
  103. data/db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb +7 -0
  104. data/db/migrate/20260711000001_add_preferences_to_spree_exports.rb +7 -0
  105. data/db/sample_data/promotions.rb +1 -1
  106. data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -1
  107. data/lib/mobility/plugins/store_based_fallbacks.rb +7 -5
  108. data/lib/spree/core/configuration.rb +1 -0
  109. data/lib/spree/core/controller_helpers/locale.rb +4 -1
  110. data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -0
  111. data/lib/spree/core/dependencies.rb +3 -0
  112. data/lib/spree/core/engine.rb +24 -0
  113. data/lib/spree/core/preferences/preferable.rb +6 -1
  114. data/lib/spree/core/previews/admin_user_preview.rb +22 -0
  115. data/lib/spree/core/previews/export_preview.rb +37 -0
  116. data/lib/spree/core/previews/invitation_preview.rb +47 -0
  117. data/lib/spree/core/previews/preview_data.rb +52 -0
  118. data/lib/spree/core/previews/report_preview.rb +40 -0
  119. data/lib/spree/core/previews/webhook_preview.rb +34 -0
  120. data/lib/spree/core/version.rb +1 -1
  121. data/lib/spree/core.rb +22 -1
  122. data/lib/spree/permitted_attributes.rb +5 -4
  123. data/lib/spree/testing_support/factories/export_factory.rb +4 -0
  124. data/lib/spree/testing_support/factories/payment_method_factory.rb +1 -9
  125. data/lib/spree/testing_support/factories/promotion_factory.rb +1 -9
  126. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +24 -0
  127. data/lib/spree/translations.rb +113 -0
  128. data/lib/spree/upgrades/5_5_to_5_6/manifest.yml +59 -0
  129. data/lib/tasks/role_users.rake +21 -0
  130. data/lib/tasks/single_store_associations.rake +65 -0
  131. data/lib/tasks/taxons.rake +62 -1
  132. metadata +47 -5
  133. data/app/models/spree/product/legacy_multi_store_support.rb +0 -40
@@ -17,19 +17,11 @@ FactoryBot.define do
17
17
 
18
18
  factory :promotion, class: Spree::Promotion do
19
19
  name { 'Promo' }
20
+ store { Spree::Store.find_by(default: true) || association(:store) }
20
21
  sequence :code do |n|
21
22
  "CODE-#{n}"
22
23
  end
23
24
 
24
- before(:create) do |promotion, _evaluator|
25
- if promotion.stores.empty?
26
- default_store = Spree::Store.default.persisted? ? Spree::Store.default : nil
27
- store = default_store || create(:store)
28
-
29
- promotion.stores << [store]
30
- end
31
- end
32
-
33
25
  trait :with_line_item_adjustment do
34
26
  transient do
35
27
  adjustment_rate { 10 }
@@ -30,4 +30,28 @@ FactoryBot.define do
30
30
  customer_group_ids { [] }
31
31
  end
32
32
  end
33
+
34
+ factory :promotion_rule_channel, class: Spree::Promotion::Rules::Channel do
35
+ association :promotion
36
+
37
+ after(:build) do |rule, evaluator|
38
+ rule.preferred_channel_ids = evaluator.channel_ids if evaluator.respond_to?(:channel_ids)
39
+ end
40
+
41
+ transient do
42
+ channel_ids { [] }
43
+ end
44
+ end
45
+
46
+ factory :promotion_rule_market, class: Spree::Promotion::Rules::Market do
47
+ association :promotion
48
+
49
+ after(:build) do |rule, evaluator|
50
+ rule.preferred_market_ids = evaluator.market_ids if evaluator.respond_to?(:market_ids)
51
+ end
52
+
53
+ transient do
54
+ market_ids { [] }
55
+ end
56
+ end
33
57
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # Builds the admin translation matrix, field-discovery metadata, and the
5
+ # public translatable-resource registry for records in
6
+ # +Spree.translatable_resources+. Stateless helper shared by the dedicated
7
+ # translations endpoint, the +?expand=translations+ serializer attribute, the
8
+ # batch write controller, and the discovery endpoint.
9
+ module Translations
10
+ module_function
11
+
12
+ # @return [Hash{String=>Hash}] locale => { field => value, "translated_field_count" => Integer }
13
+ def matrix_for(record, locales: nil)
14
+ fields = field_keys(record)
15
+ locales ||= non_default_locales(record)
16
+
17
+ locales.index_with do |locale|
18
+ translated = field_values(record, locale, fields)
19
+ translated.merge('translated_field_count' => translated.count { |_k, v| v.present? })
20
+ end
21
+ end
22
+
23
+ # @return [Array<Hash>] [{ "key" => "name", "type" => "string", "source" => "Espresso Machine" }, ...]
24
+ def fields_for(record)
25
+ Mobility.with_locale(default_locale(record)) do
26
+ field_keys(record).map do |field|
27
+ { 'key' => field, 'type' => field_type(record.class, field), 'source' => record.public_send(field) }
28
+ end
29
+ end
30
+ end
31
+
32
+ # @return [Array<Hash>] registry made public: [{ "resource_type" => "product", "fields" => [{key,type}] }]
33
+ def registry
34
+ Spree.translatable_resources.map do |klass|
35
+ {
36
+ 'resource_type' => public_resource_type(klass),
37
+ 'fields' => klass.public_translatable_fields.map { |f| { 'key' => f.to_s, 'type' => field_type(klass, f) } }
38
+ }
39
+ end
40
+ end
41
+
42
+ # @return [String] underscored, demodulized model name (e.g. "option_type")
43
+ def resource_type(klass)
44
+ klass.name.demodulize.underscore
45
+ end
46
+
47
+ # The PUBLIC resource-type token used in read (document/registry) and
48
+ # write (batch) payloads. Taxon is exposed as "category" (routes use the
49
+ # 5.5 rename) while the model element stays "taxon"; this keeps the read
50
+ # and write contracts consistent.
51
+ #
52
+ # @return [String]
53
+ def public_resource_type(klass)
54
+ return 'category' if klass <= Spree::Taxon
55
+
56
+ resource_type(klass)
57
+ end
58
+
59
+ # Inverse of +public_resource_type+: maps a public token to its
60
+ # registered translatable class, or nil if not translatable.
61
+ #
62
+ # @param token [String, Symbol] e.g. "product", "option_value", "category"
63
+ # @return [Class, nil]
64
+ def resource_class(token)
65
+ # Recomputed per call (not memoized) so a dev-mode class reload of a
66
+ # registry member doesn't leave a stale class reference behind.
67
+ map = Spree.translatable_resources.index_by { |klass| public_resource_type(klass) }
68
+ map[token.to_s]
69
+ end
70
+
71
+ # The editor type for a translatable field: +html+ when the model declares
72
+ # it as rich text (drives a rich-text editor in the SPA), else +string+.
73
+ # @param klass [Class] a translatable model
74
+ # @param field [String, Symbol] public field name
75
+ # @return [String]
76
+ def field_type(klass, field)
77
+ klass.translatable_rich_text_fields.map(&:to_sym).include?(field.to_sym) ? 'html' : 'string'
78
+ end
79
+
80
+ # Public field names, so the matrix read/write keys match the serializer
81
+ # (e.g. OptionType exposes `label`, not the internal `presentation`).
82
+ def field_keys(record)
83
+ record.class.public_translatable_fields.map(&:to_s)
84
+ end
85
+ private_class_method :field_keys
86
+
87
+ def field_values(record, locale, fields)
88
+ Mobility.with_locale(locale) do
89
+ fields.index_with do |field|
90
+ # fallback: false so an absent translation reads as nil, not the source value
91
+ record.public_send(field, fallback: false)
92
+ rescue ArgumentError
93
+ # fields without a Mobility reader signature still respond to the bare getter
94
+ record.public_send(field)
95
+ end
96
+ end
97
+ end
98
+ private_class_method :field_values
99
+
100
+ def default_locale(record)
101
+ record.translatable_store&.default_locale || I18n.default_locale.to_s
102
+ end
103
+ private_class_method :default_locale
104
+
105
+ def non_default_locales(record)
106
+ store = record.translatable_store
107
+ return [] unless store
108
+
109
+ (store.supported_locales_list - [store.default_locale]).sort
110
+ end
111
+ private_class_method :non_default_locales
112
+ end
113
+ end
@@ -0,0 +1,59 @@
1
+ # Spree 5.5 → 5.6 upgrade manifest.
2
+ #
3
+ # Lists ONLY the version-specific rake tasks that perform data backfills.
4
+ # Universal upgrade steps (bundle update, db:migrate, scheduling cron jobs,
5
+ # reviewing breaking changes) are NOT in this file:
6
+ #
7
+ # - `bundle update` + `db:migrate` are handled by your deploy pipeline
8
+ # (Heroku release phase, K8s init container, Render auto-migrate,
9
+ # Capistrano deploy hook, etc.) and by the @spree/cli's `spree upgrade`
10
+ # wrapper for local development.
11
+ # - Cron scheduling, optional tunings, and human-readable behavior changes
12
+ # live in the upgrade doc at docs/developer/upgrades/5.5-to-5.6.mdx.
13
+ #
14
+ # This manifest is the machine-runnable shape — what `bin/rake spree:upgrade`
15
+ # (in production) and `spree upgrade` (in dev) execute. Every step must be a
16
+ # rake task and must be idempotent. Re-running the full manifest is safe.
17
+ ---
18
+ from: "5.5"
19
+ to: "5.6"
20
+ docs: "https://spreecommerce.org/docs/developer/upgrades/5.5-to-5.6"
21
+
22
+ steps:
23
+ - id: role_user_store_ids
24
+ name: "Backfill store_id on role assignments"
25
+ task: "spree:role_users:backfill_store_ids"
26
+ notes: |
27
+ Sets spree_role_users.store_id from the store resource so Spree::Ability
28
+ resolves roles by store. Store-scoped assignments only; extensions (e.g.
29
+ spree_multi_vendor) backfill their own resource types. Store admins stay
30
+ authorized via the spree_admin? fallback until this runs.
31
+
32
+ - id: single_store_promotions_payment_methods
33
+ name: "Backfill store_id on promotions and payment methods"
34
+ task: "spree:upgrade:populate_single_store_associations"
35
+ notes: |
36
+ Sets store_id on Spree::Promotion and Spree::PaymentMethod from the legacy
37
+ spree_promotions_stores / spree_payment_methods_stores join tables. Until
38
+ this runs, migrated rows have a NULL store_id and are hidden from
39
+ store-scoped lookups (and unavailable at checkout), so run it right after
40
+ db:migrate. Records shared across stores keep one owner (promotions:
41
+ earliest then lowest store_id; payment methods: lowest store_id) unless
42
+ spree_multi_store is installed.
43
+
44
+ - id: taxon_store_id
45
+ name: "Backfill store_id on taxons"
46
+ task: "spree:taxons:backfill_store_id"
47
+ notes: |
48
+ Sets spree_taxons.store_id from each taxon's taxonomy, then resolves
49
+ taxonomy-less rows through their parent chain. Until this runs, existing
50
+ taxons keep resolving their store via the taxonomy join (Taxon.for_store
51
+ fallback); taxonomy-less categories (Spree::Category) rely on the direct
52
+ store_id, so the backfill is required for them.
53
+
54
+ - id: taxon_products_count
55
+ name: "Backfill products_count on taxons"
56
+ task: "spree:taxons:backfill_products_count"
57
+ notes: |
58
+ Recomputes the descendant-inclusive products_count counter cache on every
59
+ taxon in batches.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :spree do
4
+ namespace :role_users do
5
+ desc <<~DESC
6
+ Backfills +spree_role_users.store_id+ for store-scoped role assignments
7
+ (resource_type = 'Spree::Store') by copying +resource_id+. Idempotent —
8
+ only rows with a null +store_id+ are touched.
9
+
10
+ Role resolution (Spree::Ability) scopes by +store_id+, so run this after
11
+ adding the column. Until it does, the +spree_admin?+ fallback keeps store
12
+ admins authorized. Role assignments on non-store resources (e.g.
13
+ Spree::Vendor) are backfilled by the owning extension.
14
+ DESC
15
+ task backfill_store_ids: :environment do
16
+ count = Spree::RoleUser.where(resource_type: Spree::Store.name, store_id: nil)
17
+ .update_all('store_id = resource_id')
18
+ puts " Backfilled store_id on #{count} store-scoped role assignment(s)."
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ namespace :spree do
2
+ namespace :upgrade do
3
+ desc <<~DESC
4
+ Populates +spree_promotions.store_id+ and +spree_payment_methods.store_id+
5
+ from the legacy +spree_promotions_stores+ / +spree_payment_methods_stores+
6
+ join tables. Idempotent — re-running skips records that already have a
7
+ +store_id+.
8
+
9
+ Run once after upgrading to Spree 5.6+. Multi-store merchants must install
10
+ +spree_multi_store+ before running; without it, a record shared across
11
+ several stores keeps only one owner (promotions: the earliest
12
+ +spree_promotions_stores+ row by +created_at+; payment methods: the lowest
13
+ +store_id+, since that join has no timestamps) and the other stores lose
14
+ the shared record. Each shared record is logged so the loss is visible.
15
+ DESC
16
+ task populate_single_store_associations: :environment do
17
+ shared = Hash.new(0)
18
+
19
+ if ActiveRecord::Base.connection.table_exists?(Spree::StorePromotion.table_name)
20
+ Spree::Promotion.where(store_id: nil).find_each do |promotion|
21
+ store_ids = Spree::StorePromotion.where(promotion_id: promotion.id).order(:created_at, :store_id).pluck(:store_id)
22
+ next if store_ids.empty?
23
+
24
+ if store_ids.size > 1
25
+ shared[:promotions] += 1
26
+ Spree::Deprecation.warn(
27
+ "Promotion #{promotion.id} was shared across #{store_ids.size} stores; " \
28
+ "assigning it to store #{store_ids.first}. Install spree_multi_store to keep sharing."
29
+ )
30
+ end
31
+
32
+ promotion.update_column(:store_id, store_ids.first)
33
+ end
34
+ else
35
+ puts " #{Spree::StorePromotion.table_name} table not found — skipping promotions."
36
+ end
37
+
38
+ if ActiveRecord::Base.connection.table_exists?(Spree::StorePaymentMethod.table_name)
39
+ # +with_deleted+: PaymentMethod is paranoid, so soft-deleted rows would
40
+ # otherwise be skipped and keep a NULL store_id.
41
+ Spree::PaymentMethod.with_deleted.where(store_id: nil).find_each do |payment_method|
42
+ store_ids = Spree::StorePaymentMethod.where(payment_method_id: payment_method.id).order(:store_id).pluck(:store_id)
43
+ next if store_ids.empty?
44
+
45
+ if store_ids.size > 1
46
+ shared[:payment_methods] += 1
47
+ Spree::Deprecation.warn(
48
+ "PaymentMethod #{payment_method.id} was shared across #{store_ids.size} stores; " \
49
+ "assigning it to store #{store_ids.first}. Install spree_multi_store to keep sharing."
50
+ )
51
+ end
52
+
53
+ payment_method.update_column(:store_id, store_ids.first)
54
+ end
55
+ else
56
+ puts " #{Spree::StorePaymentMethod.table_name} table not found — skipping payment methods."
57
+ end
58
+
59
+ if shared.values.sum.positive?
60
+ puts " #{shared[:promotions]} promotion(s) and #{shared[:payment_methods]} payment method(s) " \
61
+ "were shared across stores — only the owner store keeps them unless spree_multi_store is installed."
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,6 +1,64 @@
1
1
  namespace :spree do
2
2
  namespace :taxons do
3
- desc 'Reset counter caches (children_count, classification_count) on taxons'
3
+ desc 'Backfill spree_taxons.store_id from each taxon\'s taxonomy (or parent)'
4
+ task backfill_store_id: :environment do |_t, _args|
5
+ puts 'Backfilling taxon store_id from taxonomy...'
6
+
7
+ # Resolve per-taxonomy and update by id — never reference a joined table in
8
+ # an update_all SET clause. It is not portable: on some adapter / Rails
9
+ # version combinations (e.g. Rails 7.2 on PostgreSQL) update_all-with-join
10
+ # is rewritten to `... WHERE id IN (subquery)`, leaving the joined alias out
11
+ # of scope in the SET and raising PG::UndefinedTable ("missing FROM-clause
12
+ # entry"). It fails at parse time, even when no rows need changing.
13
+ Spree::Taxonomy.where.not(store_id: nil).find_each do |taxonomy|
14
+ Spree::Taxon.unscoped.where(store_id: nil, taxonomy_id: taxonomy.id).
15
+ in_batches(of: 1000).update_all(store_id: taxonomy.store_id)
16
+ print '.'
17
+ end
18
+
19
+ puts "\nResolving taxonomy-less taxons through the parent chain..."
20
+
21
+ # Mirror Spree::Taxon#ensure_store: taxonomy-less rows inherit their parent's
22
+ # store. Loop so a chain of unbackfilled ancestors resolves top-down — each
23
+ # pass pushes an already-resolved parent's store onto its children.
24
+ loop do
25
+ updated = 0
26
+
27
+ # Resolved parents that still have children missing a store form the
28
+ # frontier; push each store down in one UPDATE per (store, id batch).
29
+ frontier = Spree::Taxon.unscoped.where.not(store_id: nil).
30
+ where(id: Spree::Taxon.unscoped.where(store_id: nil).select(:parent_id))
31
+ frontier.pluck(:id, :store_id).group_by(&:last).each do |store_id, rows|
32
+ rows.map(&:first).each_slice(1000) do |parent_ids|
33
+ updated += Spree::Taxon.unscoped.where(store_id: nil, parent_id: parent_ids).
34
+ update_all(store_id: store_id)
35
+ end
36
+ end
37
+
38
+ print '.'
39
+ break if updated.zero?
40
+ end
41
+
42
+ puts "\nDone!"
43
+ end
44
+
45
+ # Recomputes the descendant-inclusive products_count for every taxon, in
46
+ # batches. Shared by the dedicated task and reset_counter_caches.
47
+ backfill_products_count = lambda do
48
+ Spree::Taxon.unscoped.in_batches(of: 1000) do |batch|
49
+ Spree::Taxon.recalculate_products_count(batch.pluck(:id))
50
+ print '.'
51
+ end
52
+ end
53
+
54
+ desc 'Backfill the descendant-inclusive products_count on every taxon'
55
+ task backfill_products_count: :environment do |_t, _args|
56
+ puts 'Backfilling taxon products_count...'
57
+ backfill_products_count.call
58
+ puts "\nDone!"
59
+ end
60
+
61
+ desc 'Reset counter caches (children_count, classification_count, products_count) on taxons'
4
62
  task reset_counter_caches: :environment do |_t, _args|
5
63
  puts 'Resetting taxon counter caches...'
6
64
 
@@ -13,6 +71,9 @@ namespace :spree do
13
71
  print '.'
14
72
  end
15
73
 
74
+ puts "\nRecomputing products_count..."
75
+ backfill_products_count.call
76
+
16
77
  puts "\nDone!"
17
78
  end
18
79
  end
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.5.3
4
+ version: 5.6.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-07-10 00:00:00.000000000 Z
13
+ date: 2026-07-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -847,6 +847,7 @@ files:
847
847
  - app/helpers/spree/images_helper.rb
848
848
  - app/helpers/spree/integrations_helper.rb
849
849
  - app/helpers/spree/locale_helper.rb
850
+ - app/helpers/spree/localized_names_helper.rb
850
851
  - app/helpers/spree/products_helper.rb
851
852
  - app/helpers/spree/shipment_helper.rb
852
853
  - app/javascript/spree/core/controllers/address_autocomplete_controller.js
@@ -882,8 +883,10 @@ files:
882
883
  - app/jobs/spree/variants/remove_from_incomplete_orders_job.rb
883
884
  - app/jobs/spree/variants/remove_line_item_job.rb
884
885
  - app/jobs/spree/variants/touch_job.rb
886
+ - app/mailers/spree/admin_user_mailer.rb
885
887
  - app/mailers/spree/base_mailer.rb
886
888
  - app/mailers/spree/export_mailer.rb
889
+ - app/mailers/spree/import_mailer.rb
887
890
  - app/mailers/spree/invitation_mailer.rb
888
891
  - app/mailers/spree/report_mailer.rb
889
892
  - app/mailers/spree/webhook_mailer.rb
@@ -897,6 +900,7 @@ files:
897
900
  - app/models/concerns/spree/has_image_alt_text.rb
898
901
  - app/models/concerns/spree/image_methods.rb
899
902
  - app/models/concerns/spree/integrations_concern.rb
903
+ - app/models/concerns/spree/legacy_multi_store_support.rb
900
904
  - app/models/concerns/spree/memoized_data.rb
901
905
  - app/models/concerns/spree/metadata.rb
902
906
  - app/models/concerns/spree/metafields.rb
@@ -908,6 +912,7 @@ files:
908
912
  - app/models/concerns/spree/payment_source_concern.rb
909
913
  - app/models/concerns/spree/preference_schema.rb
910
914
  - app/models/concerns/spree/prefixed_id.rb
915
+ - app/models/concerns/spree/presentation_translatable.rb
911
916
  - app/models/concerns/spree/product_scopes.rb
912
917
  - app/models/concerns/spree/publishable.rb
913
918
  - app/models/concerns/spree/ransackable_attributes.rb
@@ -965,6 +970,7 @@ files:
965
970
  - app/models/spree/calculator/tiered_percent.rb
966
971
  - app/models/spree/category.rb
967
972
  - app/models/spree/channel.rb
973
+ - app/models/spree/channel/gating.rb
968
974
  - app/models/spree/checkout/default_requirements.rb
969
975
  - app/models/spree/checkout/registry.rb
970
976
  - app/models/spree/checkout/requirement.rb
@@ -974,6 +980,7 @@ files:
974
980
  - app/models/spree/country.rb
975
981
  - app/models/spree/coupon_code.rb
976
982
  - app/models/spree/credit_card.rb
983
+ - app/models/spree/currency.rb
977
984
  - app/models/spree/current.rb
978
985
  - app/models/spree/custom_field.rb
979
986
  - app/models/spree/custom_field_definition.rb
@@ -1020,6 +1027,7 @@ files:
1020
1027
  - app/models/spree/legacy_admin_user.rb
1021
1028
  - app/models/spree/legacy_user.rb
1022
1029
  - app/models/spree/line_item.rb
1030
+ - app/models/spree/locale.rb
1023
1031
  - app/models/spree/log_entry.rb
1024
1032
  - app/models/spree/market.rb
1025
1033
  - app/models/spree/market_country.rb
@@ -1106,7 +1114,6 @@ files:
1106
1114
  - app/models/spree/price_rules/zone_rule.rb
1107
1115
  - app/models/spree/product.rb
1108
1116
  - app/models/spree/product/channels.rb
1109
- - app/models/spree/product/legacy_multi_store_support.rb
1110
1117
  - app/models/spree/product/slugs.rb
1111
1118
  - app/models/spree/product/webhooks.rb
1112
1119
  - app/models/spree/product_option_type.rb
@@ -1117,11 +1124,13 @@ files:
1117
1124
  - app/models/spree/promotion/actions/create_item_adjustments.rb
1118
1125
  - app/models/spree/promotion/actions/create_line_items.rb
1119
1126
  - app/models/spree/promotion/actions/free_shipping.rb
1127
+ - app/models/spree/promotion/rules/channel.rb
1120
1128
  - app/models/spree/promotion/rules/country.rb
1121
1129
  - app/models/spree/promotion/rules/currency.rb
1122
1130
  - app/models/spree/promotion/rules/customer_group.rb
1123
1131
  - app/models/spree/promotion/rules/first_order.rb
1124
1132
  - app/models/spree/promotion/rules/item_total.rb
1133
+ - app/models/spree/promotion/rules/market.rb
1125
1134
  - app/models/spree/promotion/rules/one_use_per_user.rb
1126
1135
  - app/models/spree/promotion/rules/option_value.rb
1127
1136
  - app/models/spree/promotion/rules/product.rb
@@ -1182,6 +1191,8 @@ files:
1182
1191
  - app/models/spree/search_provider/filters_result.rb
1183
1192
  - app/models/spree/search_provider/meilisearch.rb
1184
1193
  - app/models/spree/search_provider/search_result.rb
1194
+ - app/models/spree/setup_task.rb
1195
+ - app/models/spree/setup_tasks.rb
1185
1196
  - app/models/spree/shipment.rb
1186
1197
  - app/models/spree/shipment/custom_events.rb
1187
1198
  - app/models/spree/shipment/webhooks.rb
@@ -1236,6 +1247,7 @@ files:
1236
1247
  - app/models/spree/taxon_rules/sale.rb
1237
1248
  - app/models/spree/taxon_rules/tag.rb
1238
1249
  - app/models/spree/taxonomy.rb
1250
+ - app/models/spree/translations/batch.rb
1239
1251
  - app/models/spree/user_identity.rb
1240
1252
  - app/models/spree/variant.rb
1241
1253
  - app/models/spree/variant/webhooks.rb
@@ -1298,6 +1310,7 @@ files:
1298
1310
  - app/services/spree/coupon_codes/bulk_generate.rb
1299
1311
  - app/services/spree/coupon_codes/coupon_codes_handler.rb
1300
1312
  - app/services/spree/credit_cards/destroy.rb
1313
+ - app/services/spree/fulfillments/create.rb
1301
1314
  - app/services/spree/generate_token.rb
1302
1315
  - app/services/spree/gift_cards/apply.rb
1303
1316
  - app/services/spree/gift_cards/redeem.rb
@@ -1310,6 +1323,7 @@ files:
1310
1323
  - app/services/spree/line_items/destroy.rb
1311
1324
  - app/services/spree/line_items/helper.rb
1312
1325
  - app/services/spree/line_items/update.rb
1326
+ - app/services/spree/locales.rb
1313
1327
  - app/services/spree/locales/set_fallback_locale_for_store.rb
1314
1328
  - app/services/spree/newsletter/link_user.rb
1315
1329
  - app/services/spree/newsletter/subscribe.rb
@@ -1371,8 +1385,10 @@ files:
1371
1385
  - app/sorters/spree/base_sorter.rb
1372
1386
  - app/sorters/spree/classifications/sort.rb
1373
1387
  - app/sorters/spree/products/sort.rb
1388
+ - app/subscribers/spree/admin_user_email_subscriber.rb
1374
1389
  - app/subscribers/spree/event_log_subscriber.rb
1375
1390
  - app/subscribers/spree/export_subscriber.rb
1391
+ - app/subscribers/spree/import_email_subscriber.rb
1376
1392
  - app/subscribers/spree/invitation_email_subscriber.rb
1377
1393
  - app/subscribers/spree/product_metrics_subscriber.rb
1378
1394
  - app/subscribers/spree/report_subscriber.rb
@@ -1383,7 +1399,10 @@ files:
1383
1399
  - app/views/layouts/spree/base_mailer.html.erb
1384
1400
  - app/views/spree/addresses/_form.html.erb
1385
1401
  - app/views/spree/addresses/_suggestions_box.erb
1402
+ - app/views/spree/admin_user_mailer/confirmation_email.html.erb
1403
+ - app/views/spree/admin_user_mailer/password_reset_email.html.erb
1386
1404
  - app/views/spree/export_mailer/export_done.html.erb
1405
+ - app/views/spree/import_mailer/import_done.html.erb
1387
1406
  - app/views/spree/invitation_mailer/invitation_accepted.html.erb
1388
1407
  - app/views/spree/invitation_mailer/invitation_email.html.erb
1389
1408
  - app/views/spree/report_mailer/report_done.html.erb
@@ -1391,9 +1410,12 @@ files:
1391
1410
  - app/views/spree/shared/_base_mailer_header.html.erb
1392
1411
  - app/views/spree/shared/_base_mailer_stylesheets.html.erb
1393
1412
  - app/views/spree/shared/_google_places_api_loader.html.erb
1413
+ - app/views/spree/shared/_mailer_button.html.erb
1414
+ - app/views/spree/shared/_mailer_hero.html.erb
1394
1415
  - app/views/spree/shared/_mailer_line_item.html.erb
1395
1416
  - app/views/spree/shared/_mailer_logo.html.erb
1396
1417
  - app/views/spree/shared/_payment.html.erb
1418
+ - app/views/spree/shared/_purchased_items_styles.html.erb
1397
1419
  - app/views/spree/webhook_mailer/endpoint_disabled.html.erb
1398
1420
  - app/views/spree/webhook_mailer/endpoint_disabled.text.erb
1399
1421
  - config/brakeman.ignore
@@ -1406,6 +1428,7 @@ files:
1406
1428
  - config/initializers/inflections.rb
1407
1429
  - config/initializers/mobility.rb
1408
1430
  - config/initializers/rails61_fixes.rb
1431
+ - config/initializers/spree_store_setup_tasks.rb
1409
1432
  - config/locales/en.yml
1410
1433
  - config/routes.rb
1411
1434
  - db/migrate/20210914000000_spree_four_three.rb
@@ -1568,6 +1591,15 @@ files:
1568
1591
  - db/migrate/20260601000002_add_store_id_to_spree_products.rb
1569
1592
  - db/migrate/20260602000001_add_default_to_spree_channels.rb
1570
1593
  - db/migrate/20260612000001_change_spree_user_identities_info_to_jsonb.rb
1594
+ - db/migrate/20260613000001_add_store_id_to_spree_role_users.rb
1595
+ - db/migrate/20260626000001_add_store_id_to_spree_taxons.rb
1596
+ - db/migrate/20260627000001_add_products_count_to_spree_taxons.rb
1597
+ - db/migrate/20260628000001_add_store_id_to_spree_promotions.rb
1598
+ - db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb
1599
+ - db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb
1600
+ - db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb
1601
+ - db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb
1602
+ - db/migrate/20260711000001_add_preferences_to_spree_exports.rb
1571
1603
  - db/sample_data/channels.rb
1572
1604
  - db/sample_data/customers.csv
1573
1605
  - db/sample_data/markets.rb
@@ -1643,6 +1675,12 @@ files:
1643
1675
  - lib/spree/core/preferences/runtime_configuration.rb
1644
1676
  - lib/spree/core/preferences/scoped_store.rb
1645
1677
  - lib/spree/core/preferences/store.rb
1678
+ - lib/spree/core/previews/admin_user_preview.rb
1679
+ - lib/spree/core/previews/export_preview.rb
1680
+ - lib/spree/core/previews/invitation_preview.rb
1681
+ - lib/spree/core/previews/preview_data.rb
1682
+ - lib/spree/core/previews/report_preview.rb
1683
+ - lib/spree/core/previews/webhook_preview.rb
1646
1684
  - lib/spree/core/pricing/context.rb
1647
1685
  - lib/spree/core/pricing/resolver.rb
1648
1686
  - lib/spree/core/ransack_configuration.rb
@@ -1780,7 +1818,9 @@ files:
1780
1818
  - lib/spree/testing_support/store.rb
1781
1819
  - lib/spree/testing_support/url_helpers.rb
1782
1820
  - lib/spree/translation_migrations.rb
1821
+ - lib/spree/translations.rb
1783
1822
  - lib/spree/upgrades/5_4_to_5_5/manifest.yml
1823
+ - lib/spree/upgrades/5_5_to_5_6/manifest.yml
1784
1824
  - lib/spree/webhooks.rb
1785
1825
  - lib/spree_core.rb
1786
1826
  - lib/tasks/channels.rake
@@ -1792,8 +1832,10 @@ files:
1792
1832
  - lib/tasks/price_history.rake
1793
1833
  - lib/tasks/products.rake
1794
1834
  - lib/tasks/publications.rake
1835
+ - lib/tasks/role_users.rake
1795
1836
  - lib/tasks/sample_data.rake
1796
1837
  - lib/tasks/search.rake
1838
+ - lib/tasks/single_store_associations.rake
1797
1839
  - lib/tasks/taxons.rake
1798
1840
  - lib/tasks/upgrade.rake
1799
1841
  - lib/tasks/variants.rake
@@ -1818,9 +1860,9 @@ licenses:
1818
1860
  - BSD-3-Clause
1819
1861
  metadata:
1820
1862
  bug_tracker_uri: https://github.com/spree/spree/issues
1821
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.3
1863
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc1
1822
1864
  documentation_uri: https://docs.spreecommerce.org/
1823
- source_code_uri: https://github.com/spree/spree/tree/v5.5.3
1865
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc1
1824
1866
  post_install_message:
1825
1867
  rdoc_options: []
1826
1868
  require_paths:
@@ -1,40 +0,0 @@
1
- module Spree
2
- class Product
3
- # Legacy multi-store support. In Spree 5.5+ the +Spree::ProductPublication+ model
4
- # handles the Product↔Store relation, so this module only provides a fallback
5
- # for legacy code that still references +Spree::Product#stores+.
6
- module LegacyMultiStoreSupport
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- def stores
11
- Spree::Deprecation.warn(
12
- "Spree::Product#stores is deprecated. Please use Spree::Product.store instead. If you want to continue using multiple stores please install spree_multi_store gem"
13
- )
14
- store ? [store] : []
15
- end
16
-
17
- def store_ids
18
- Spree::Deprecation.warn(
19
- "Spree::Product#store_ids is deprecated. Please use Spree::Product.store_id instead. If you want to continue using multiple stores please install spree_multi_store gem"
20
- )
21
- store_id ? [store_id] : []
22
- end
23
-
24
- def stores=(stores)
25
- Spree::Deprecation.warn(
26
- "Spree::Product#stores= is deprecated. Please use Spree::Product.store= instead. If you want to continue using multiple stores please install spree_multi_store gem"
27
- )
28
- self.store = Array(stores).compact.first
29
- end
30
-
31
- def store_ids=(store_ids)
32
- Spree::Deprecation.warn(
33
- "Spree::Product#store_ids= is deprecated. Please use Spree::Product.store_id= instead. If you want to continue using multiple stores please install spree_multi_store gem"
34
- )
35
- self.store_id = Array(store_ids).compact_blank.first
36
- end
37
- end
38
- end
39
- end
40
- end