spree_core 5.5.3 → 5.6.0.rc2

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 (140) 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 +47 -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/newsletter_subscriber.rb +4 -0
  41. data/app/models/spree/option_type.rb +11 -13
  42. data/app/models/spree/option_value.rb +5 -17
  43. data/app/models/spree/order.rb +19 -2
  44. data/app/models/spree/payment_method.rb +6 -5
  45. data/app/models/spree/permission_sets/default_customer.rb +3 -0
  46. data/app/models/spree/policy.rb +2 -1
  47. data/app/models/spree/product.rb +23 -2
  48. data/app/models/spree/promotion/rules/channel.rb +37 -0
  49. data/app/models/spree/promotion/rules/market.rb +37 -0
  50. data/app/models/spree/promotion.rb +5 -3
  51. data/app/models/spree/promotion_handler/promotion_duplicator.rb +1 -1
  52. data/app/models/spree/role_user.rb +4 -0
  53. data/app/models/spree/search_provider/meilisearch.rb +22 -9
  54. data/app/models/spree/setup_task.rb +12 -0
  55. data/app/models/spree/setup_tasks.rb +86 -0
  56. data/app/models/spree/shipment.rb +38 -1
  57. data/app/models/spree/shipment_handler.rb +1 -1
  58. data/app/models/spree/stock/availability_validator.rb +1 -1
  59. data/app/models/spree/stock/quantifier.rb +21 -1
  60. data/app/models/spree/stock_item.rb +2 -1
  61. data/app/models/spree/stock_location.rb +4 -1
  62. data/app/models/spree/store.rb +45 -9
  63. data/app/models/spree/store_payment_method.rb +5 -1
  64. data/app/models/spree/store_promotion.rb +4 -0
  65. data/app/models/spree/taxon.rb +112 -9
  66. data/app/models/spree/taxonomy.rb +1 -1
  67. data/app/models/spree/translations/batch.rb +104 -0
  68. data/app/models/spree/user_identity.rb +35 -24
  69. data/app/models/spree/variant.rb +29 -2
  70. data/app/presenters/spree/search_provider/product_presenter.rb +3 -0
  71. data/app/services/spree/carts/complete.rb +3 -0
  72. data/app/services/spree/fulfillments/create.rb +259 -0
  73. data/app/services/spree/locales/set_fallback_locale_for_store.rb +7 -1
  74. data/app/services/spree/locales.rb +52 -0
  75. data/app/services/spree/newsletter/subscribe.rb +1 -0
  76. data/app/services/spree/products/prepare_nested_attributes.rb +40 -12
  77. data/app/services/spree/shipments/update.rb +1 -1
  78. data/app/services/spree/stock_reservations/reserve.rb +4 -1
  79. data/app/services/spree/taxons/add_products.rb +3 -0
  80. data/app/services/spree/taxons/remove_products.rb +3 -0
  81. data/app/subscribers/spree/admin_user_email_subscriber.rb +29 -0
  82. data/app/subscribers/spree/import_email_subscriber.rb +26 -0
  83. data/app/views/layouts/spree/base_mailer.html.erb +14 -17
  84. data/app/views/spree/admin_user_mailer/confirmation_email.html.erb +7 -0
  85. data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +8 -0
  86. data/app/views/spree/export_mailer/export_done.html.erb +6 -14
  87. data/app/views/spree/import_mailer/import_done.html.erb +12 -0
  88. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +4 -12
  89. data/app/views/spree/invitation_mailer/invitation_email.html.erb +11 -24
  90. data/app/views/spree/report_mailer/report_done.html.erb +6 -14
  91. data/app/views/spree/shared/_base_mailer_footer.html.erb +15 -8
  92. data/app/views/spree/shared/_base_mailer_header.html.erb +7 -5
  93. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +211 -273
  94. data/app/views/spree/shared/_mailer_button.html.erb +10 -0
  95. data/app/views/spree/shared/_mailer_hero.html.erb +12 -0
  96. data/app/views/spree/shared/_purchased_items_styles.html.erb +112 -0
  97. data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +39 -14
  98. data/config/initializers/spree_store_setup_tasks.rb +23 -0
  99. data/config/locales/en.yml +42 -992
  100. data/db/migrate/20260613000001_add_store_id_to_spree_role_users.rb +10 -0
  101. data/db/migrate/20260626000001_add_store_id_to_spree_taxons.rb +11 -0
  102. data/db/migrate/20260627000001_add_products_count_to_spree_taxons.rb +6 -0
  103. data/db/migrate/20260628000001_add_store_id_to_spree_promotions.rb +10 -0
  104. data/db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb +11 -0
  105. data/db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb +7 -0
  106. data/db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb +39 -0
  107. data/db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb +7 -0
  108. data/db/migrate/20260711000001_add_preferences_to_spree_exports.rb +7 -0
  109. data/db/sample_data/promotions.rb +1 -1
  110. data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -1
  111. data/lib/mobility/plugins/store_based_fallbacks.rb +7 -5
  112. data/lib/spree/core/configuration.rb +1 -0
  113. data/lib/spree/core/controller_helpers/locale.rb +4 -1
  114. data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -0
  115. data/lib/spree/core/dependencies.rb +3 -0
  116. data/lib/spree/core/engine.rb +24 -0
  117. data/lib/spree/core/preferences/preferable.rb +6 -1
  118. data/lib/spree/core/previews/admin_user_preview.rb +22 -0
  119. data/lib/spree/core/previews/export_preview.rb +37 -0
  120. data/lib/spree/core/previews/invitation_preview.rb +47 -0
  121. data/lib/spree/core/previews/preview_data.rb +52 -0
  122. data/lib/spree/core/previews/report_preview.rb +40 -0
  123. data/lib/spree/core/previews/webhook_preview.rb +34 -0
  124. data/lib/spree/core/version.rb +1 -1
  125. data/lib/spree/core.rb +38 -1
  126. data/lib/spree/permitted_attributes.rb +5 -4
  127. data/lib/spree/testing_support/factories/export_factory.rb +4 -0
  128. data/lib/spree/testing_support/factories/order_factory.rb +1 -1
  129. data/lib/spree/testing_support/factories/payment_factory.rb +3 -3
  130. data/lib/spree/testing_support/factories/payment_method_factory.rb +1 -9
  131. data/lib/spree/testing_support/factories/payment_session_factory.rb +2 -2
  132. data/lib/spree/testing_support/factories/promotion_factory.rb +1 -9
  133. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +24 -0
  134. data/lib/spree/translations.rb +113 -0
  135. data/lib/spree/upgrades/5_5_to_5_6/manifest.yml +59 -0
  136. data/lib/tasks/role_users.rake +21 -0
  137. data/lib/tasks/single_store_associations.rake +65 -0
  138. data/lib/tasks/taxons.rake +62 -1
  139. metadata +47 -5
  140. data/app/models/spree/product/legacy_multi_store_support.rb +0 -40
@@ -5,7 +5,7 @@ FactoryBot.define do
5
5
  state { 'checkout' }
6
6
  response_code { "BGS-#{SecureRandom.hex(6)}" }
7
7
 
8
- payment_method { create(:credit_card_payment_method, stores: [order.store]) }
8
+ payment_method { create(:credit_card_payment_method, store: order.store) }
9
9
  association(:source, factory: :credit_card)
10
10
 
11
11
  factory :payment_with_refund do
@@ -16,7 +16,7 @@ FactoryBot.define do
16
16
  end
17
17
 
18
18
  factory :custom_payment, class: Spree::Payment do
19
- payment_method { create(:custom_payment_method, stores: [order.store]) }
19
+ payment_method { create(:custom_payment_method, store: order.store) }
20
20
  source { create(:payment_source, user: order.user, payment_method: payment_method) }
21
21
  end
22
22
  end
@@ -29,7 +29,7 @@ FactoryBot.define do
29
29
  end
30
30
 
31
31
  factory :store_credit_payment, class: Spree::Payment, parent: :payment do
32
- payment_method { create(:store_credit_payment_method, stores: [order.store]) }
32
+ payment_method { create(:store_credit_payment_method, store: order.store) }
33
33
  source { create(:store_credit, store: order.store, user: order.user) }
34
34
  end
35
35
  end
@@ -2,15 +2,7 @@ FactoryBot.define do
2
2
  factory :payment_method, class: Spree::PaymentMethod do
3
3
  type { 'Spree::PaymentMethod' }
4
4
  name { 'Test' }
5
-
6
- before(:create) do |payment_method|
7
- if payment_method.stores.empty?
8
- default_store = Spree::Store.default.persisted? ? Spree::Store.default : nil
9
- store = default_store || create(:store)
10
-
11
- payment_method.stores << store
12
- end
13
- end
5
+ store { Spree::Store.find_by(default: true) || association(:store) }
14
6
  end
15
7
 
16
8
  factory :check_payment_method, parent: :payment_method, class: Spree::PaymentMethod::Check do
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
  factory :payment_session, class: 'Spree::PaymentSession' do
3
3
  order
4
- payment_method { create(:credit_card_payment_method, stores: [order.store]) }
4
+ payment_method { create(:credit_card_payment_method, store: order.store) }
5
5
  amount { order.total }
6
6
  currency { order.currency }
7
7
  status { 'pending' }
@@ -39,7 +39,7 @@ FactoryBot.define do
39
39
 
40
40
  factory :bogus_payment_session, class: 'Spree::PaymentSessions::Bogus' do
41
41
  type { 'Spree::PaymentSessions::Bogus' }
42
- payment_method { create(:bogus_payment_method, stores: [order.store]) }
42
+ payment_method { create(:bogus_payment_method, store: order.store) }
43
43
  external_id { "bogus_#{SecureRandom.hex(12)}" }
44
44
  external_data { { 'client_secret' => "bogus_secret_#{SecureRandom.hex(8)}" } }
45
45
  end
@@ -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