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
@@ -0,0 +1,10 @@
1
+ class AddStoreIdToSpreeRoleUsers < ActiveRecord::Migration[7.2]
2
+ def change
3
+ # Denormalizes the store a role assignment applies within, so role
4
+ # resolution (Spree::Ability) can scope by store without depending on the
5
+ # polymorphic resource. Kept null: true here; existing rows are backfilled
6
+ # by `spree:role_users:backfill_store_ids` and presence is enforced
7
+ # on the model.
8
+ add_reference :spree_role_users, :store, null: true
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class AddStoreIdToSpreeTaxons < ActiveRecord::Migration[7.2]
2
+ # NOTE: After running this migration, existing taxons have +store_id IS NULL+
3
+ # and keep resolving their store through their taxonomy (Taxon.for_store falls
4
+ # back to the taxonomy join). Run the backfill to populate +store_id+ directly,
5
+ # which is what taxonomy-less categories (Spree::Category) rely on:
6
+ #
7
+ # bundle exec rake spree:taxons:backfill_store_id
8
+ def change
9
+ add_reference :spree_taxons, :store, null: true, if_not_exists: true
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ class AddProductsCountToSpreeTaxons < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_taxons, :products_count, :integer, default: 0, null: false, if_not_exists: true
4
+ add_index :spree_taxons, :products_count, if_not_exists: true
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ class AddStoreIdToSpreePromotions < ActiveRecord::Migration[7.2]
2
+ # NOTE: After running this migration, existing promotions have +store_id IS NULL+
3
+ # and are invisible to +Promotion.for_store+. Run the backfill immediately to
4
+ # copy ownership from the legacy +spree_promotions_stores+ join table:
5
+ #
6
+ # bundle exec rake spree:upgrade:populate_single_store_associations
7
+ def change
8
+ add_reference :spree_promotions, :store, null: true, if_not_exists: true
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class AddStoreIdToSpreePaymentMethods < ActiveRecord::Migration[7.2]
2
+ # NOTE: After running this migration, existing payment methods have
3
+ # +store_id IS NULL+ and are invisible to +PaymentMethod.for_store+. Run the
4
+ # backfill immediately to copy ownership from the legacy
5
+ # +spree_payment_methods_stores+ join table:
6
+ #
7
+ # bundle exec rake spree:upgrade:populate_single_store_associations
8
+ def change
9
+ add_reference :spree_payment_methods, :store, null: true, if_not_exists: true
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class AddPreorderFieldsToSpreeVariants < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_variants, :preorderable, :boolean
4
+ add_column :spree_variants, :preorder_ships_at, :datetime
5
+ add_column :spree_variants, :backorder_limit, :integer
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ class AddFingerprintToSpreeCreditCards < ActiveRecord::Migration[7.2]
2
+ INDEX_NAME = 'index_spree_credit_cards_unique_fingerprint'.freeze
3
+
4
+ def up
5
+ add_column :spree_credit_cards, :fingerprint, :string
6
+
7
+ # Prevent duplicate saved cards (same gateway fingerprint + expiry) per user
8
+ # and payment method at the database level, backing the application-level
9
+ # check against concurrent writes. Only active, fingerprinted cards are
10
+ # constrained, so legacy/non-gateway cards (NULL fingerprint) and
11
+ # soft-deleted rows are left untouched.
12
+ if ActiveRecord::Base.connection.adapter_name == 'Mysql2'
13
+ # MySQL has no partial indexes, but treats NULL as distinct in unique
14
+ # indexes, so NULL fingerprints are naturally allowed. COALESCE on
15
+ # deleted_at keeps soft-deleted rows from colliding with active ones.
16
+ execute <<-SQL
17
+ CREATE UNIQUE INDEX #{INDEX_NAME}
18
+ ON spree_credit_cards(
19
+ user_id,
20
+ payment_method_id,
21
+ fingerprint,
22
+ month,
23
+ year,
24
+ (COALESCE(deleted_at, CAST('1970-01-01' AS DATETIME)))
25
+ )
26
+ SQL
27
+ else
28
+ add_index :spree_credit_cards, [:user_id, :payment_method_id, :fingerprint, :month, :year],
29
+ unique: true,
30
+ where: 'fingerprint IS NOT NULL AND deleted_at IS NULL',
31
+ name: INDEX_NAME
32
+ end
33
+ end
34
+
35
+ def down
36
+ remove_index :spree_credit_cards, name: INDEX_NAME
37
+ remove_column :spree_credit_cards, :fingerprint
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ class AddImportIdStatusIndexToSpreeImportRows < ActiveRecord::Migration[7.2]
2
+ def change
3
+ # Powers the Admin API's per-poll grouped status counts and the
4
+ # failed-rows listing without a full scan on large imports.
5
+ add_index :spree_import_rows, [:import_id, :status]
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class AddPreferencesToSpreeExports < ActiveRecord::Migration[7.2]
2
+ def change
3
+ # Same serialized preferences store spree_imports already has — first
4
+ # consumer is the `results_url` the export-done email links back to.
5
+ add_column :spree_exports, :preferences, :text
6
+ end
7
+ end
@@ -2,7 +2,7 @@ promotion = Spree::Promotion.where(
2
2
  name: 'Free Shipping',
3
3
  code: 'FREESHIP'
4
4
  ).first_or_create! do |promo|
5
- promo.stores = Spree::Store.all
5
+ promo.store = Spree::Store.default
6
6
  end
7
7
 
8
8
  Spree::Promotion::Actions::FreeShipping.where(promotion: promotion).first_or_create!
@@ -2,7 +2,6 @@ class CreateSpreeAdminUsers < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :spree_admin_users do |t|
4
4
  t.string :encrypted_password, limit: 128
5
- t.string :password_salt, limit: 128
6
5
  t.string :email
7
6
  t.string :remember_token
8
7
  t.string :persistence_token
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
4
3
  require 'mobility'
5
4
 
6
5
  # The default implementation in the Mobility gem requires fallbacks to be defined when booting the application.
@@ -8,15 +7,18 @@ require 'mobility'
8
7
  # The implementation is based on the default fallbacks plugin, with some changes around fetching the list of fallbacks to be used.
9
8
  # https://github.com/shioyama/mobility/blob/master/lib/mobility/plugins/fallbacks.rb
10
9
  module Mobility
11
- @store_based_fallbacks = Concurrent::ThreadLocalVar.new(I18n::Locale::Fallbacks.new)
12
-
13
10
  class << self
11
+ # Request-scoped via RequestStore (like Mobility.locale), so one request's
12
+ # store fallbacks can't leak into the next request served by the same
13
+ # thread. Outside a request RequestStore degrades to plain per-thread
14
+ # storage; Spree::BaseMailer#with_store_locale restores the previous value
15
+ # around mail rendering.
14
16
  def store_based_fallbacks
15
- @store_based_fallbacks.value
17
+ RequestStore.store[:mobility_store_based_fallbacks] ||= I18n::Locale::Fallbacks.new
16
18
  end
17
19
 
18
20
  def store_based_fallbacks=(value)
19
- @store_based_fallbacks.value = value
21
+ RequestStore.store[:mobility_store_based_fallbacks] = value
20
22
  end
21
23
  end
22
24
 
@@ -118,6 +118,7 @@ module Spree
118
118
  preference :coupon_codes_total_limit, :integer, default: 5000 # the maximum number of coupon codes to be generated
119
119
 
120
120
  # password reset
121
+ preference :admin_password_reset_expires_in, :integer, default: 15 # admin password reset token expiration time in minutes
121
122
  preference :customer_password_reset_expires_in, :integer, default: 15 # password reset token expiration time in minutes
122
123
 
123
124
  # gift cards
@@ -19,7 +19,10 @@ module Spree
19
19
  end
20
20
 
21
21
  def set_locale
22
- I18n.default_locale = default_locale unless Spree.always_use_translations?
22
+ # Request-local content locale — the fallback for storeless hosts
23
+ # lives in the Spree::Current#content_locale reader; never write the
24
+ # process-global I18n.default_locale, which all threads share.
25
+ Spree::Current.content_locale = current_store&.default_locale
23
26
  I18n.locale = current_locale
24
27
  end
25
28
 
@@ -33,6 +33,7 @@ module Spree
33
33
 
34
34
  def permitted_product_attributes
35
35
  permitted_attributes.product_attributes + [
36
+ removed_variant_ids: [],
36
37
  variants_attributes: permitted_variant_attributes + ['id', :_destroy],
37
38
  master_attributes: permitted_variant_attributes + ['id']
38
39
  ]
@@ -49,6 +49,9 @@ module Spree
49
49
  order_update_service: 'Spree::Orders::Update',
50
50
  order_updater: 'Spree::OrderUpdater',
51
51
 
52
+ # fulfillment
53
+ fulfillment_create_service: 'Spree::Fulfillments::Create',
54
+
52
55
  # shipment
53
56
  shipment_change_state_service: 'Spree::Shipments::ChangeState',
54
57
  shipment_create_service: 'Spree::Shipments::Create',
@@ -46,6 +46,14 @@ module Spree
46
46
  # Add app/subscribers to autoload paths
47
47
  config.paths.add 'app/subscribers', eager_load: true
48
48
 
49
+ # Register bundled ActionMailer previews so they show up at /rails/mailers
50
+ # without the host app having to copy any files.
51
+ initializer 'spree.mailer_previews' do |app|
52
+ if app.config.action_mailer.show_previews
53
+ app.config.action_mailer.preview_paths << File.expand_path('previews', __dir__)
54
+ end
55
+ end
56
+
49
57
  initializer 'spree.environment', before: :load_config_initializers do |app|
50
58
  app.config.spree = Environment.new(SpreeCalculators.new, SpreeValidators.new, Spree::Core::Configuration.new, Spree::Core::Dependencies.new)
51
59
 
@@ -57,6 +65,18 @@ module Spree
57
65
  Spree::Deprecation = ActiveSupport::Deprecation.new('6.0', 'Spree')
58
66
  end
59
67
 
68
+ # I18n's config lives in fiber/thread-local storage that survives across
69
+ # requests on reused server threads, so a request that never assigns its
70
+ # own locale would render in whatever locale the previous request on the
71
+ # same thread set. The i18n gem ships a middleware that resets it after
72
+ # every request; Rails does not install it by default. Mobility's request
73
+ # state needs no counterpart here — Mobility.locale and Spree's
74
+ # Mobility.store_based_fallbacks live in RequestStore, which is cleared
75
+ # per request by request_store's own middleware.
76
+ initializer 'spree.locale_state_reset' do |app|
77
+ app.middleware.use ::I18n::Middleware
78
+ end
79
+
60
80
  initializer 'spree.register.subscribers', before: :load_config_initializers do |app|
61
81
  # Initialize subscribers array early so engines can add subscribers via initializers
62
82
  app.config.spree.subscribers = []
@@ -179,6 +199,8 @@ module Spree
179
199
  Rails.application.config.spree.promotions.rules.concat [
180
200
  Spree::Promotion::Rules::Currency,
181
201
  Spree::Promotion::Rules::Country,
202
+ Spree::Promotion::Rules::Channel,
203
+ Spree::Promotion::Rules::Market,
182
204
  Spree::Promotion::Rules::ItemTotal,
183
205
  Spree::Promotion::Rules::Product,
184
206
  Spree::Promotion::Rules::User,
@@ -245,6 +267,7 @@ module Spree
245
267
 
246
268
  Rails.application.config.spree.translatable_resources = [
247
269
  Spree::OptionType,
270
+ Spree::OptionValue,
248
271
  Spree::Product,
249
272
  Spree::Taxon,
250
273
  Spree::Taxonomy,
@@ -345,6 +368,7 @@ module Spree
345
368
  Spree::ExportSubscriber,
346
369
  Spree::ReportSubscriber,
347
370
  Spree::InvitationEmailSubscriber,
371
+ Spree::AdminUserEmailSubscriber,
348
372
  Spree::ProductMetricsSubscriber
349
373
  ]
350
374
 
@@ -135,7 +135,12 @@ module Spree::Preferences::Preferable
135
135
  int_value ||= 0 unless nullable
136
136
  int_value.present? ? int_value.to_i : int_value
137
137
  when :boolean
138
- if value.is_a?(FalseClass) ||
138
+ # A nullable boolean keeps "unset" (nil / empty string) as nil so it can
139
+ # fall back to another value, instead of collapsing it to false. An
140
+ # explicit false is preserved (it is neither nil nor empty).
141
+ if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
142
+ nil
143
+ elsif value.is_a?(FalseClass) ||
139
144
  value.nil? ||
140
145
  value == 0 ||
141
146
  value&.to_s =~ /^(f|false|0)$/i ||
@@ -0,0 +1,22 @@
1
+ require_relative 'preview_data'
2
+
3
+ # Preview Spree admin user auth emails at /rails/mailers/spree/admin_user
4
+ class Spree::AdminUserPreview < ActionMailer::Preview
5
+ include Spree::PreviewData::LocaleParam
6
+
7
+ def password_reset_email
8
+ Spree::AdminUserMailer.password_reset_email(
9
+ Spree::PreviewData.admin_user,
10
+ 'preview-token',
11
+ Spree::PreviewData.store(locale)
12
+ )
13
+ end
14
+
15
+ def confirmation_email
16
+ Spree::AdminUserMailer.confirmation_email(
17
+ Spree::PreviewData.admin_user,
18
+ 'preview-token',
19
+ Spree::PreviewData.store(locale)
20
+ )
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'preview_data'
2
+
3
+ # Preview Spree export emails at /rails/mailers/spree/export
4
+ class Spree::ExportPreview < ActionMailer::Preview
5
+ include Spree::PreviewData::LocaleParam
6
+
7
+ def export_done
8
+ Spree::ExportMailer.export_done(export)
9
+ end
10
+
11
+ private
12
+
13
+ # Reuse the most recent export, or build an in-memory example so the preview
14
+ # works on a database that has never run an export. When the preview toolbar
15
+ # requests a locale, always use the example so its store carries that locale.
16
+ # The example is never saved, so no `export.created` side effects fire.
17
+ def export
18
+ return example_export if locale.present?
19
+
20
+ Spree::Export.last || example_export
21
+ end
22
+
23
+ def example_export
24
+ export = Spree::Exports::Products.new(
25
+ id: 0,
26
+ store: Spree::PreviewData.store(locale),
27
+ user: Spree::PreviewData.admin_user,
28
+ format: :csv
29
+ )
30
+ export.attachment.attach(
31
+ io: StringIO.new("id,name,price\n1,Example Product,19.99\n"),
32
+ filename: 'products-export.csv',
33
+ content_type: 'text/csv'
34
+ )
35
+ export
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'preview_data'
2
+
3
+ # Preview Spree invitation emails at /rails/mailers/spree/invitation
4
+ class Spree::InvitationPreview < ActionMailer::Preview
5
+ include Spree::PreviewData::LocaleParam
6
+
7
+ def invitation_email
8
+ Spree::InvitationMailer.invitation_email(pending_invitation)
9
+ end
10
+
11
+ def invitation_accepted
12
+ Spree::InvitationMailer.invitation_accepted(accepted_invitation)
13
+ end
14
+
15
+ private
16
+
17
+ def pending_invitation
18
+ return example_invitation if locale.present?
19
+
20
+ Spree::Invitation.pending.last || example_invitation
21
+ end
22
+
23
+ def accepted_invitation
24
+ return example_invitation(accepted: true) if locale.present?
25
+
26
+ Spree::Invitation.accepted.last || example_invitation(accepted: true)
27
+ end
28
+
29
+ # Build an in-memory invitation so the preview works on a database with no
30
+ # invitations. When the preview toolbar requests a locale, its store carries
31
+ # that locale. Never saved, so no records are created.
32
+ def example_invitation(accepted: false)
33
+ store = Spree::PreviewData.store(locale)
34
+ admin = Spree::PreviewData.admin_user
35
+ Spree::Invitation.new(
36
+ id: 0,
37
+ email: 'invitee@example.com',
38
+ resource: store,
39
+ inviter: admin,
40
+ invitee: accepted ? admin : nil,
41
+ role: Spree::Role.first || Spree::Role.new(name: 'admin'),
42
+ token: 'preview-token',
43
+ status: accepted ? 'accepted' : 'pending',
44
+ expires_at: 7.days.from_now
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,52 @@
1
+ # Shared helpers for ActionMailer previews. Everything here is in-memory (never
2
+ # saved) so opening a mailer preview stays read-only.
3
+ module Spree
4
+ module PreviewData
5
+ # Include in ActionMailer::Preview subclasses to read the preview toolbar's
6
+ # locale dropdown (`?locale=`). The toolbar sends uppercase codes ("DE"),
7
+ # Spree locales are lowercase.
8
+ module LocaleParam
9
+ private
10
+
11
+ def locale
12
+ @params[:locale]&.downcase
13
+ end
14
+ end
15
+
16
+ module_function
17
+
18
+ # An admin user for previews that need `user.email` / `user.first_name`.
19
+ # Falls back to an unsaved instance when the database has no admin users.
20
+ def admin_user
21
+ Spree.admin_user_class.first ||
22
+ Spree.admin_user_class.new(email: 'admin@example.com', first_name: 'Alex', last_name: 'Doe')
23
+ end
24
+
25
+ # A customer for previews of customer-facing account emails. Falls back to
26
+ # an unsaved instance when the database has no users.
27
+ def customer
28
+ Spree.user_class.first ||
29
+ Spree.user_class.new(email: 'customer@example.com').tap do |user|
30
+ user.first_name = 'Alex' if user.respond_to?(:first_name=)
31
+ end
32
+ end
33
+
34
+ # The store previews render for. When the preview toolbar's locale dropdown
35
+ # sets `?locale=`, return an unsaved copy of the default store with that
36
+ # `default_locale`, so mailers that render in `store.default_locale` honour
37
+ # the toggle without mutating the real store.
38
+ #
39
+ # @param locale [String, Symbol, nil] the `?locale=` param
40
+ # @return [Spree::Store]
41
+ def store(locale = nil)
42
+ store = Spree::Store.default
43
+ return store if locale.blank? || store.nil?
44
+
45
+ dup = store.dup
46
+ dup.id = store.id # keep prefixed-id / URL helpers working
47
+ dup.default_locale = locale.to_s.downcase
48
+ dup.readonly!
49
+ dup
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'preview_data'
2
+
3
+ # Preview Spree report emails at /rails/mailers/spree/report
4
+ class Spree::ReportPreview < ActionMailer::Preview
5
+ include Spree::PreviewData::LocaleParam
6
+
7
+ def report_done
8
+ Spree::ReportMailer.report_done(report)
9
+ end
10
+
11
+ private
12
+
13
+ # Reuse the most recent report, or build an in-memory example so the preview
14
+ # works on a database that has never run a report. When the preview toolbar
15
+ # requests a locale, always use the example so its store carries that locale.
16
+ # The example is never saved, so no `report.created` side effects fire.
17
+ def report
18
+ return example_report if locale.present?
19
+
20
+ Spree::Report.last || example_report
21
+ end
22
+
23
+ def example_report
24
+ store = Spree::PreviewData.store(locale)
25
+ report = Spree::Reports::SalesTotal.new(
26
+ id: 0,
27
+ store: store,
28
+ user: Spree::PreviewData.admin_user,
29
+ date_from: 30.days.ago.to_date,
30
+ date_to: Date.current,
31
+ currency: store.default_currency
32
+ )
33
+ report.attachment.attach(
34
+ io: StringIO.new("date,total\n2026-01-01,1234.56\n"),
35
+ filename: 'sales-total.csv',
36
+ content_type: 'text/csv'
37
+ )
38
+ report
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'preview_data'
2
+
3
+ # Preview Spree webhook notification emails at /rails/mailers/spree/webhook
4
+ class Spree::WebhookPreview < ActionMailer::Preview
5
+ include Spree::PreviewData::LocaleParam
6
+
7
+ def endpoint_disabled
8
+ Spree::WebhookMailer.endpoint_disabled(webhook_endpoint)
9
+ end
10
+
11
+ private
12
+
13
+ # Reuse the most recent endpoint, or build an in-memory disabled example so the
14
+ # preview works on a database with no webhook endpoints. When the preview
15
+ # toolbar requests a locale, always use the example so its store carries that
16
+ # locale. Never saved, so the admin webhook list stays clean.
17
+ def webhook_endpoint
18
+ return example_endpoint if locale.present?
19
+
20
+ Spree::WebhookEndpoint.last || example_endpoint
21
+ end
22
+
23
+ def example_endpoint
24
+ Spree::WebhookEndpoint.new(
25
+ store: Spree::PreviewData.store(locale),
26
+ name: 'Example endpoint',
27
+ url: 'https://example.com/webhooks/spree',
28
+ active: false,
29
+ disabled_reason: 'Too many failed delivery attempts',
30
+ disabled_at: Time.current,
31
+ subscriptions: ['order.completed']
32
+ )
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.5.3'.freeze
2
+ VERSION = '5.6.0.rc1'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/spree/core.rb CHANGED
@@ -165,7 +165,19 @@ module Spree
165
165
  end
166
166
 
167
167
  def self.use_translations?
168
- Spree::Config.always_use_translations || I18n.default_locale != I18n.locale
168
+ Spree::Config.always_use_translations || Spree::Current.content_locale != I18n.locale.name
169
+ end
170
+
171
+ # Mobility +column_fallback+ option shared by every translatable model:
172
+ # read, write and query the base (untranslated) column when the target
173
+ # locale is the request's content locale — see Spree::Current#content_locale.
174
+ # Evaluated on every translated attribute read, so it must stay
175
+ # allocation-free and must not touch the database (Symbol#name returns the
176
+ # frozen interned string; Mobility always passes Symbol locales).
177
+ def self.mobility_column_fallback
178
+ return false if always_use_translations?
179
+
180
+ ->(locale) { locale.name == Spree::Current.content_locale }
169
181
  end
170
182
 
171
183
  # Used to configure Spree.
@@ -321,6 +333,14 @@ module Spree
321
333
  Rails.application.config.spree.reports = value
322
334
  end
323
335
 
336
+ # Registry of the Getting Started onboarding tasks shown on the admin
337
+ # dashboard. See {Spree::SetupTasks} for the extension API.
338
+ #
339
+ # @return [Spree::SetupTasks]
340
+ def self.store_setup_tasks
341
+ @store_setup_tasks ||= Spree::SetupTasks.new
342
+ end
343
+
324
344
  def self.translatable_resources
325
345
  Rails.application.config.spree.translatable_resources
326
346
  end
@@ -493,6 +513,7 @@ require 'spree/core/engine'
493
513
 
494
514
  require 'spree/i18n'
495
515
  require 'spree/localized_number'
516
+ require 'spree/translations'
496
517
  require 'spree/money'
497
518
  require 'spree/permitted_attributes'
498
519
  require 'spree/service_module'
@@ -198,7 +198,7 @@ module Spree
198
198
  ]
199
199
 
200
200
  @@promotion_attributes = [:name, :description, :starts_at, :expires_at, :code, :usage_limit, :path, :match_policy,
201
- :advertise, :promotion_category_id, :code_prefix, :kind, :number_of_codes, :multi_codes, store_ids: []]
201
+ :advertise, :promotion_category_id, :code_prefix, :kind, :number_of_codes, :multi_codes]
202
202
 
203
203
  @@promotion_rule_attributes = [:type, :preferred_match_policy, preferred_eligible_values: [], user_ids_to_add: [], product_ids_to_add: [], taxon_ids_to_add: []]
204
204
 
@@ -240,7 +240,8 @@ module Spree
240
240
 
241
241
  @@shipping_category_attributes = [:name]
242
242
 
243
- @@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy]
243
+ @@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy,
244
+ :preferred_storefront_access, :preferred_guest_checkout]
244
245
 
245
246
  @@shipping_method_attributes = [:name, :admin_name, :code, :tracking_url, :tax_category_id, :display_on,
246
247
  :estimated_transit_business_days_min, :estimated_transit_business_days_max,
@@ -278,7 +279,7 @@ module Spree
278
279
  :preferred_limit_digital_download_count, :preferred_limit_digital_download_days,
279
280
  :preferred_digital_asset_link_expire_time,
280
281
  :logo, :mailer_logo,
281
- :preferred_guest_checkout]
282
+ :preferred_guest_checkout, :preferred_storefront_access]
282
283
 
283
284
  @@store_credit_attributes = %i[amount currency category_id memo]
284
285
 
@@ -307,7 +308,7 @@ module Spree
307
308
 
308
309
  @@variant_attributes = [
309
310
  :name, :presentation, :cost_price, :discontinue_on, :lock_version,
310
- :position, :track_inventory, :tax_category_id,
311
+ :position, :track_inventory, :preorderable, :preorder_ships_at, :backorder_limit, :tax_category_id,
311
312
  :product_id, :product, :option_values_attributes, :price, :compare_at_price,
312
313
  :weight, :height, :width, :depth, :sku, :barcode, :cost_currency,
313
314
  :weight_unit, :dimensions_unit,
@@ -16,5 +16,9 @@ FactoryBot.define do
16
16
  factory :customer_export, class: 'Spree::Exports::Customers', parent: :export do
17
17
  type { 'Spree::Exports::Customers' }
18
18
  end
19
+
20
+ factory :coupon_code_export, class: 'Spree::Exports::CouponCodes', parent: :export do
21
+ type { 'Spree::Exports::CouponCodes' }
22
+ end
19
23
  end
20
24
  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