spree_core 5.5.2 → 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.
- checksums.yaml +4 -4
- data/app/helpers/spree/base_helper.rb +12 -2
- data/app/helpers/spree/currency_helper.rb +1 -4
- data/app/helpers/spree/locale_helper.rb +18 -13
- data/app/helpers/spree/localized_names_helper.rb +25 -0
- data/app/jobs/spree/imports/process_group_job.rb +20 -9
- data/app/jobs/spree/imports/process_rows_job.rb +12 -4
- data/app/mailers/spree/admin_user_mailer.rb +71 -0
- data/app/mailers/spree/base_mailer.rb +73 -11
- data/app/mailers/spree/export_mailer.rb +7 -7
- data/app/mailers/spree/import_mailer.rb +19 -0
- data/app/mailers/spree/invitation_mailer.rb +16 -11
- data/app/mailers/spree/report_mailer.rb +6 -6
- data/app/mailers/spree/webhook_mailer.rb +7 -6
- data/app/models/concerns/spree/admin_user_methods.rb +44 -0
- data/app/models/concerns/spree/legacy_multi_store_support.rb +48 -0
- data/app/models/concerns/spree/presentation_translatable.rb +39 -0
- data/app/models/concerns/spree/product_scopes.rb +21 -3
- data/app/models/concerns/spree/single_store_resource.rb +5 -0
- data/app/models/concerns/spree/store_scoped_resource.rb +10 -0
- data/app/models/concerns/spree/stores/markets.rb +9 -1
- data/app/models/concerns/spree/stores/setup.rb +37 -23
- data/app/models/concerns/spree/translatable_resource.rb +95 -5
- data/app/models/concerns/spree/user_methods.rb +27 -2
- data/app/models/spree/ability.rb +5 -4
- data/app/models/spree/allowed_origin.rb +31 -0
- data/app/models/spree/category.rb +12 -2
- data/app/models/spree/channel/gating.rb +67 -0
- data/app/models/spree/channel.rb +1 -0
- data/app/models/spree/classification.rb +12 -0
- data/app/models/spree/country.rb +22 -1
- data/app/models/spree/credit_card.rb +29 -0
- data/app/models/spree/currency.rb +47 -0
- data/app/models/spree/current.rb +12 -1
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/export.rb +19 -0
- data/app/models/spree/exports/coupon_codes.rb +1 -2
- data/app/models/spree/import.rb +93 -1
- data/app/models/spree/import_row.rb +7 -1
- data/app/models/spree/imports/product_translations.rb +6 -0
- data/app/models/spree/locale.rb +104 -0
- data/app/models/spree/option_type.rb +11 -13
- data/app/models/spree/option_value.rb +5 -17
- data/app/models/spree/order.rb +19 -2
- data/app/models/spree/payment_method.rb +6 -5
- data/app/models/spree/policy.rb +2 -1
- data/app/models/spree/product.rb +23 -2
- data/app/models/spree/promotion/rules/channel.rb +37 -0
- data/app/models/spree/promotion/rules/market.rb +37 -0
- data/app/models/spree/promotion.rb +5 -3
- data/app/models/spree/promotion_handler/promotion_duplicator.rb +1 -1
- data/app/models/spree/role_user.rb +4 -0
- data/app/models/spree/search_provider/meilisearch.rb +22 -9
- data/app/models/spree/setup_task.rb +12 -0
- data/app/models/spree/setup_tasks.rb +86 -0
- data/app/models/spree/shipment.rb +38 -1
- data/app/models/spree/shipment_handler.rb +1 -1
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +21 -1
- data/app/models/spree/stock_item.rb +2 -1
- data/app/models/spree/stock_location.rb +4 -1
- data/app/models/spree/store.rb +45 -9
- data/app/models/spree/store_payment_method.rb +5 -1
- data/app/models/spree/store_promotion.rb +4 -0
- data/app/models/spree/taxon.rb +112 -9
- data/app/models/spree/taxonomy.rb +1 -1
- data/app/models/spree/translations/batch.rb +104 -0
- data/app/models/spree/variant.rb +29 -2
- data/app/presenters/spree/search_provider/product_presenter.rb +3 -0
- data/app/services/spree/carts/complete.rb +3 -0
- data/app/services/spree/fulfillments/create.rb +259 -0
- data/app/services/spree/imports/row_processors/base.rb +9 -0
- data/app/services/spree/imports/row_processors/product_variant.rb +37 -19
- data/app/services/spree/locales/set_fallback_locale_for_store.rb +7 -1
- data/app/services/spree/locales.rb +52 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +40 -12
- data/app/services/spree/shipments/update.rb +1 -1
- data/app/services/spree/stock_reservations/reserve.rb +4 -1
- data/app/services/spree/taxons/add_products.rb +3 -0
- data/app/services/spree/taxons/remove_products.rb +3 -0
- data/app/subscribers/spree/admin_user_email_subscriber.rb +29 -0
- data/app/subscribers/spree/import_email_subscriber.rb +26 -0
- data/app/views/layouts/spree/base_mailer.html.erb +14 -17
- data/app/views/spree/admin_user_mailer/confirmation_email.html.erb +7 -0
- data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +8 -0
- data/app/views/spree/export_mailer/export_done.html.erb +6 -14
- data/app/views/spree/import_mailer/import_done.html.erb +12 -0
- data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +4 -12
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +11 -24
- data/app/views/spree/report_mailer/report_done.html.erb +6 -14
- data/app/views/spree/shared/_base_mailer_footer.html.erb +15 -8
- data/app/views/spree/shared/_base_mailer_header.html.erb +7 -5
- data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +211 -273
- data/app/views/spree/shared/_mailer_button.html.erb +10 -0
- data/app/views/spree/shared/_mailer_hero.html.erb +12 -0
- data/app/views/spree/shared/_purchased_items_styles.html.erb +112 -0
- data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +39 -14
- data/config/initializers/spree_store_setup_tasks.rb +23 -0
- data/config/locales/en.yml +42 -992
- data/db/migrate/20260613000001_add_store_id_to_spree_role_users.rb +10 -0
- data/db/migrate/20260626000001_add_store_id_to_spree_taxons.rb +11 -0
- data/db/migrate/20260627000001_add_products_count_to_spree_taxons.rb +6 -0
- data/db/migrate/20260628000001_add_store_id_to_spree_promotions.rb +10 -0
- data/db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb +11 -0
- data/db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb +7 -0
- data/db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb +39 -0
- data/db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb +7 -0
- data/db/migrate/20260711000001_add_preferences_to_spree_exports.rb +7 -0
- data/db/sample_data/promotions.rb +1 -1
- data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -1
- data/lib/mobility/plugins/store_based_fallbacks.rb +7 -5
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/locale.rb +4 -1
- data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -0
- data/lib/spree/core/dependencies.rb +3 -0
- data/lib/spree/core/engine.rb +24 -0
- data/lib/spree/core/preferences/preferable.rb +6 -1
- data/lib/spree/core/previews/admin_user_preview.rb +22 -0
- data/lib/spree/core/previews/export_preview.rb +37 -0
- data/lib/spree/core/previews/invitation_preview.rb +47 -0
- data/lib/spree/core/previews/preview_data.rb +52 -0
- data/lib/spree/core/previews/report_preview.rb +40 -0
- data/lib/spree/core/previews/webhook_preview.rb +34 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +22 -1
- data/lib/spree/permitted_attributes.rb +6 -5
- data/lib/spree/testing_support/factories/export_factory.rb +4 -0
- data/lib/spree/testing_support/factories/payment_method_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_rule_factory.rb +24 -0
- data/lib/spree/translations.rb +113 -0
- data/lib/spree/upgrades/5_5_to_5_6/manifest.yml +59 -0
- data/lib/tasks/role_users.rake +21 -0
- data/lib/tasks/single_store_associations.rake +65 -0
- data/lib/tasks/taxons.rake +62 -1
- metadata +47 -5
- data/app/models/spree/product/legacy_multi_store_support.rb +0 -40
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# A rule to limit a promotion based on the order's market.
|
|
2
|
+
module Spree
|
|
3
|
+
class Promotion
|
|
4
|
+
module Rules
|
|
5
|
+
class Market < PromotionRule
|
|
6
|
+
# Stored as raw IDs. Accepts prefixed IDs (`mkt_…`) from API
|
|
7
|
+
# callers and decodes them on write so eligibility checks compare
|
|
8
|
+
# against the order's raw `market_id` directly. Scope confines the
|
|
9
|
+
# existence check to the promotion's store so cross-store market
|
|
10
|
+
# IDs can't sneak in.
|
|
11
|
+
preference :market_ids, :array, default: [],
|
|
12
|
+
parse_on_set: normalize_id_preference(
|
|
13
|
+
klass: Spree::Market,
|
|
14
|
+
scope: ->(rule) { rule.promotion&.store&.markets || Spree::Market.none }
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def applicable?(promotable)
|
|
18
|
+
promotable.is_a?(Spree::Order)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def markets
|
|
22
|
+
return Spree::Market.none if preferred_market_ids.blank?
|
|
23
|
+
|
|
24
|
+
promotion.store.markets.where(id: preferred_market_ids)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def eligible?(order, _options = {})
|
|
28
|
+
return false if preferred_market_ids.empty?
|
|
29
|
+
return true if preferred_market_ids.map(&:to_s).include?(order.market_id.to_s)
|
|
30
|
+
|
|
31
|
+
eligibility_errors.add(:base, eligibility_error_message(:no_matching_market))
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -2,12 +2,14 @@ module Spree
|
|
|
2
2
|
class Promotion < Spree.base_class
|
|
3
3
|
has_prefix_id :promo # Spree-specific: promotion
|
|
4
4
|
|
|
5
|
-
include Spree::
|
|
5
|
+
include Spree::SingleStoreResource
|
|
6
6
|
include Spree::Metafields
|
|
7
7
|
include Spree::Metadata
|
|
8
8
|
if defined?(Spree::Security::Promotions)
|
|
9
9
|
include Spree::Security::Promotions
|
|
10
10
|
end
|
|
11
|
+
# Multi-store sharing moved to the spree_multi_store extension in 5.6.
|
|
12
|
+
include Spree::LegacyMultiStoreSupport unless defined?(SpreeMultiStore)
|
|
11
13
|
|
|
12
14
|
publishes_lifecycle_events
|
|
13
15
|
|
|
@@ -37,8 +39,7 @@ module Spree
|
|
|
37
39
|
has_many :coupon_codes, -> { order(created_at: :asc) }, dependent: :destroy, class_name: 'Spree::CouponCode'
|
|
38
40
|
has_many :order_promotions, class_name: 'Spree::OrderPromotion'
|
|
39
41
|
has_many :orders, through: :order_promotions, class_name: 'Spree::Order'
|
|
40
|
-
|
|
41
|
-
has_many :stores, class_name: 'Spree::Store', through: :store_promotions
|
|
42
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
42
43
|
|
|
43
44
|
after_save :apply_pending_rules_and_actions, if: :pending_rules_or_actions?
|
|
44
45
|
|
|
@@ -60,6 +61,7 @@ module Spree
|
|
|
60
61
|
#
|
|
61
62
|
validates_associated :rules
|
|
62
63
|
validates :name, presence: true
|
|
64
|
+
validates :store, presence: true, unless: -> { Spree::Config[:disable_store_presence_validation] }
|
|
63
65
|
validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
|
|
64
66
|
validates :description, length: { maximum: 255 }, allow_blank: true
|
|
65
67
|
validate :expires_at_must_be_later_than_starts_at, if: -> { starts_at && expires_at }
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
@new_promotion.path = "#{@promotion.path}_#{@random_string}"
|
|
12
12
|
@new_promotion.name = "New #{@promotion.name}"
|
|
13
13
|
@new_promotion.code = "#{@promotion.code}_#{@random_string}"
|
|
14
|
-
@new_promotion.
|
|
14
|
+
@new_promotion.store = @promotion.store
|
|
15
15
|
|
|
16
16
|
ActiveRecord::Base.transaction do
|
|
17
17
|
@new_promotion.save
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class RoleUser < Spree.base_class
|
|
3
|
+
include Spree::SingleStoreResource
|
|
4
|
+
|
|
3
5
|
#
|
|
4
6
|
# Associations
|
|
5
7
|
#
|
|
6
8
|
belongs_to :role, class_name: 'Spree::Role', foreign_key: :role_id
|
|
7
9
|
belongs_to :user, polymorphic: true
|
|
8
10
|
belongs_to :resource, polymorphic: true
|
|
11
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
9
12
|
belongs_to :invitation, class_name: 'Spree::Invitation', optional: true, inverse_of: :role_user
|
|
10
13
|
|
|
11
14
|
#
|
|
@@ -14,6 +17,7 @@ module Spree
|
|
|
14
17
|
validates :role, presence: true
|
|
15
18
|
validates :user, presence: true
|
|
16
19
|
validates :resource, presence: true
|
|
20
|
+
validates :store, presence: true
|
|
17
21
|
validates :role_id, uniqueness: { scope: [:user_id, :resource_id, :user_type, :resource_type] }
|
|
18
22
|
|
|
19
23
|
#
|
|
@@ -158,6 +158,7 @@ module Spree
|
|
|
158
158
|
|
|
159
159
|
Rails.logger.debug { "[Meilisearch] index=#{index_name} query=#{query.inspect} #{search_params.compact.inspect}" }
|
|
160
160
|
|
|
161
|
+
settings_refreshed = false
|
|
161
162
|
begin
|
|
162
163
|
if return_facets && grouped_options.any?
|
|
163
164
|
queries = [{ indexUid: index_name, q: query.to_s, **search_params }]
|
|
@@ -175,6 +176,16 @@ module Spree
|
|
|
175
176
|
facet_distribution = ms_result['facetDistribution'] || {}
|
|
176
177
|
end
|
|
177
178
|
rescue ::Meilisearch::ApiError => e
|
|
179
|
+
# Self-heal when a newly added filterable attribute (e.g. `preorder`)
|
|
180
|
+
# is referenced before the index settings were refreshed — otherwise
|
|
181
|
+
# every search on an upgraded store returns empty until a reindex or
|
|
182
|
+
# the next product write.
|
|
183
|
+
if e.code == 'invalid_search_filter' && !settings_refreshed
|
|
184
|
+
settings_refreshed = true
|
|
185
|
+
ensure_index_settings!
|
|
186
|
+
retry
|
|
187
|
+
end
|
|
188
|
+
|
|
178
189
|
Rails.logger.warn { "[Meilisearch] Search failed: #{e.message}. Run `rake spree:search:reindex` to initialize the index." }
|
|
179
190
|
Rails.error.report(e, handled: true, context: { index: index_name, query: query })
|
|
180
191
|
return nil
|
|
@@ -233,7 +244,7 @@ module Spree
|
|
|
233
244
|
end
|
|
234
245
|
|
|
235
246
|
def filterable_attributes
|
|
236
|
-
%w[product_id status in_stock store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids]
|
|
247
|
+
%w[product_id status in_stock preorder store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids]
|
|
237
248
|
end
|
|
238
249
|
|
|
239
250
|
def sortable_attributes
|
|
@@ -266,14 +277,16 @@ module Spree
|
|
|
266
277
|
conditions << "status = 'active'"
|
|
267
278
|
conditions << "locale = '#{locale.to_s.gsub(/[^a-zA-Z_-]/, '')}'"
|
|
268
279
|
conditions << "currency = '#{currency.to_s.gsub(/[^A-Z]/, '')}'"
|
|
269
|
-
# Exclude future-dated products — mirrors
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
|
|
280
|
+
# Exclude future-dated products — mirrors
|
|
281
|
+
# +Product.available(Time.current, include_preorderable: true)+. ISO 8601
|
|
282
|
+
# strings sort lexicographically in chronological order, so the string
|
|
283
|
+
# compare is sound; +NOT EXISTS+/+IS NULL+ keep the available_on clause
|
|
284
|
+
# backward-compatible with legacy docs. The trailing +preorder = true+
|
|
285
|
+
# keeps scheduled "coming soon" pre-orders searchable before their
|
|
286
|
+
# publish date. It filters on the new +preorder+ attribute, so a
|
|
287
|
+
# +rake spree:search:reindex+ (or the next product write, which refreshes
|
|
288
|
+
# the index settings) is required after deploy before it takes effect.
|
|
289
|
+
conditions << "(available_on NOT EXISTS OR available_on IS NULL OR available_on <= '#{now.iso8601}' OR preorder = true)"
|
|
277
290
|
conditions << "(discontinue_on = 0 OR discontinue_on > #{now.to_i})"
|
|
278
291
|
conditions
|
|
279
292
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# An evaluated onboarding task — the serializable result of running a
|
|
3
|
+
# {Spree::SetupTasks} registry entry against its subject (e.g. a store).
|
|
4
|
+
# See {Spree::Store#setup_tasks}.
|
|
5
|
+
class SetupTask
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
include ActiveModel::Attributes
|
|
8
|
+
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
attribute :done, :boolean
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Registry of onboarding tasks for a given subject type. `Spree.store_setup_tasks`
|
|
3
|
+
# holds the Getting Started checklist evaluated against a store; future
|
|
4
|
+
# registries (e.g. vendor onboarding) instantiate the same class against
|
|
5
|
+
# their own subject. Defaults are registered in
|
|
6
|
+
# `config/initializers/spree_store_setup_tasks.rb`; extensions and host apps
|
|
7
|
+
# can add or remove tasks at boot:
|
|
8
|
+
#
|
|
9
|
+
# Rails.application.config.after_initialize do
|
|
10
|
+
# Spree.store_setup_tasks.add :connect_stripe,
|
|
11
|
+
# position: 25,
|
|
12
|
+
# done: ->(store) { store.payment_methods.active.exists?(type: 'Spree::PaymentMethod::Stripe') }
|
|
13
|
+
#
|
|
14
|
+
# Spree.store_setup_tasks.remove :setup_taxes_collection
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The registry holds domain data only — each frontend maps task keys to its
|
|
18
|
+
# own presentation: the legacy admin renders the
|
|
19
|
+
# `spree/admin/dashboard/setup_tasks/_<key>` partial titled by the
|
|
20
|
+
# `admin.store_setup_tasks.<key>` translation (both resolved across engine
|
|
21
|
+
# paths, so extensions ship theirs by convention), and the React dashboard
|
|
22
|
+
# consumes tasks as {Spree::SetupTask} records via the Admin API.
|
|
23
|
+
class SetupTasks
|
|
24
|
+
class Definition
|
|
25
|
+
attr_reader :key, :position
|
|
26
|
+
|
|
27
|
+
def initialize(key, position:, done:, **options)
|
|
28
|
+
raise ArgumentError, "done: must respond to #call" unless done.respond_to?(:call)
|
|
29
|
+
|
|
30
|
+
@key = key.to_sym
|
|
31
|
+
@position = position
|
|
32
|
+
@done = done
|
|
33
|
+
@if = options[:if]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param subject [Object] the record the checklist belongs to (e.g. a store)
|
|
37
|
+
# @return [Boolean] whether the task is complete
|
|
38
|
+
def done?(subject)
|
|
39
|
+
!!@done.call(subject)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param subject [Object] the record the checklist belongs to
|
|
43
|
+
# @return [Boolean] whether the task applies to this subject at all
|
|
44
|
+
def available?(subject)
|
|
45
|
+
@if.nil? || !!@if.call(subject)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize
|
|
50
|
+
@tasks = {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Registers a task (replacing any existing task with the same key).
|
|
54
|
+
#
|
|
55
|
+
# @param key [Symbol] identifies the task; frontends derive presentation from it
|
|
56
|
+
# @param position [Integer] sort order in the checklist
|
|
57
|
+
# @param done [#call] receives the subject, returns whether the task is complete
|
|
58
|
+
# @param options [Hash] optional :if (per-subject visibility callable)
|
|
59
|
+
# @return [Definition]
|
|
60
|
+
def add(key, position:, done:, **options)
|
|
61
|
+
key = key.to_sym
|
|
62
|
+
@tasks[key] = Definition.new(key, position: position, done: done, **options)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param key [Symbol]
|
|
66
|
+
# @return [Definition, nil] the removed task
|
|
67
|
+
def remove(key)
|
|
68
|
+
@tasks.delete(key.to_sym)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find(key)
|
|
72
|
+
@tasks[key.to_sym]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Array<Definition>] all tasks sorted by position
|
|
76
|
+
def tasks
|
|
77
|
+
@tasks.values.sort_by(&:position)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param subject [Object] the record the checklist belongs to (e.g. a store)
|
|
81
|
+
# @return [Array<Definition>] the tasks applicable to the given subject, sorted by position
|
|
82
|
+
def for(subject)
|
|
83
|
+
tasks.select { |task| task.available?(subject) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -130,6 +130,17 @@ module Spree
|
|
|
130
130
|
cost
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
# Strict decimal bridge for string input — raises ArgumentError on
|
|
134
|
+
# malformed values instead of letting Rails' cast silently truncate
|
|
135
|
+
# ("12 boxes" would otherwise become 12.0). Blank strings cast to nil,
|
|
136
|
+
# matching the default Rails behavior.
|
|
137
|
+
#
|
|
138
|
+
# @param value [String, Numeric, nil]
|
|
139
|
+
def cost=(value)
|
|
140
|
+
value = value.blank? ? nil : BigDecimal(value.strip) if value.is_a?(String)
|
|
141
|
+
super
|
|
142
|
+
end
|
|
143
|
+
|
|
133
144
|
def digital?
|
|
134
145
|
shipping_method&.digital? || false
|
|
135
146
|
end
|
|
@@ -347,6 +358,23 @@ module Spree
|
|
|
347
358
|
shipping_rates
|
|
348
359
|
end
|
|
349
360
|
|
|
361
|
+
# Public API v3 name for the selected rate (see docs/plans/6.0-fulfillment-and-delivery.md).
|
|
362
|
+
#
|
|
363
|
+
# @return [String, nil] the selected delivery rate's prefixed ID
|
|
364
|
+
def selected_delivery_rate_id
|
|
365
|
+
selected_shipping_rate&.prefixed_id
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Selects a delivery rate by its public prefixed ID (+dr_...+); raw IDs
|
|
369
|
+
# are accepted for internal callers. The model owns this naming bridge so
|
|
370
|
+
# API clients never deal with the legacy shipping-rate name.
|
|
371
|
+
#
|
|
372
|
+
# @param id [String, Integer] delivery rate prefixed or raw ID
|
|
373
|
+
def selected_delivery_rate_id=(id)
|
|
374
|
+
rate = Spree::PrefixedId.prefixed_id?(id) ? shipping_rates.find_by_prefix_id!(id) : shipping_rates.find(id)
|
|
375
|
+
self.selected_shipping_rate_id = rate.id
|
|
376
|
+
end
|
|
377
|
+
|
|
350
378
|
def selected_shipping_rate_id
|
|
351
379
|
selected_shipping_rate.try(:id)
|
|
352
380
|
end
|
|
@@ -422,8 +450,17 @@ module Spree
|
|
|
422
450
|
package
|
|
423
451
|
end
|
|
424
452
|
|
|
453
|
+
# External systems (3PLs, courier APIs) often hand over a complete
|
|
454
|
+
# tracking link rather than a bare tracking code — returned as-is
|
|
455
|
+
# instead of being templated into the delivery method's tracking URL.
|
|
456
|
+
#
|
|
457
|
+
# @return [String, nil]
|
|
425
458
|
def tracking_url
|
|
426
|
-
@tracking_url ||=
|
|
459
|
+
@tracking_url ||= if tracking&.start_with?('https://')
|
|
460
|
+
tracking
|
|
461
|
+
else
|
|
462
|
+
shipping_method&.build_tracking_url(tracking)
|
|
463
|
+
end
|
|
427
464
|
end
|
|
428
465
|
|
|
429
466
|
def update_amounts
|
|
@@ -7,7 +7,7 @@ module Spree
|
|
|
7
7
|
# Do we have a specialized shipping-method-specific handler? e.g:
|
|
8
8
|
# Given shipment.shipping_method = Spree::ShippingMethod::DigitalDownload
|
|
9
9
|
# do we have Spree::ShipmentHandler::DigitalDownload?
|
|
10
|
-
if sm_handler = "Spree::ShipmentHandler::#{shipment.shipping_method
|
|
10
|
+
if sm_handler = "Spree::ShipmentHandler::#{shipment.shipping_method&.name&.split('::')&.last}".safe_constantize
|
|
11
11
|
sm_handler.new(shipment)
|
|
12
12
|
else
|
|
13
13
|
new(shipment)
|
|
@@ -14,7 +14,7 @@ module Spree
|
|
|
14
14
|
display_name = variant.name.to_s
|
|
15
15
|
display_name += " (#{variant.options_text})" unless variant.options_text.blank?
|
|
16
16
|
|
|
17
|
-
if variant.available?
|
|
17
|
+
if variant.available? || variant.preorder?
|
|
18
18
|
line_item.errors.add(:quantity,
|
|
19
19
|
:selected_quantity_not_available,
|
|
20
20
|
message: Spree.t(:selected_quantity_not_available, item: display_name.inspect))
|
|
@@ -88,8 +88,28 @@ module Spree
|
|
|
88
88
|
@backorderable ||= stock_items.any?(&:backorderable)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Whether the requested quantity can be supplied. Beyond on-hand stock a
|
|
92
|
+
# variant may oversell two ways: +backorderable+ stock, or a pre-order
|
|
93
|
+
# (which also lifts the publish gate for a scheduled launch). Either way
|
|
94
|
+
# the variant's +backorder_limit+ caps how far below zero it may go; a nil
|
|
95
|
+
# (empty) limit means unlimited.
|
|
91
96
|
def can_supply?(required = 1)
|
|
92
|
-
variant.available?
|
|
97
|
+
return false unless variant.available? || variant.preorder?
|
|
98
|
+
return true unless variant.should_track_inventory?
|
|
99
|
+
|
|
100
|
+
oversellable = backorderable? || variant.preorder?
|
|
101
|
+
limit = variant.backorder_limit
|
|
102
|
+
return true if oversellable && limit.nil?
|
|
103
|
+
|
|
104
|
+
# On-hand stock, plus — for a capped oversell — the room to sell below
|
|
105
|
+
# zero down to +-backorder_limit+ (a single clamp of signed on-hand plus
|
|
106
|
+
# the limit).
|
|
107
|
+
supplyable = if oversellable
|
|
108
|
+
[available_stock - reserved_quantity + limit, 0].max
|
|
109
|
+
else
|
|
110
|
+
total_on_hand
|
|
111
|
+
end
|
|
112
|
+
supplyable >= required
|
|
93
113
|
end
|
|
94
114
|
|
|
95
115
|
def stock_items
|
|
@@ -107,7 +107,8 @@ module Spree
|
|
|
107
107
|
private
|
|
108
108
|
|
|
109
109
|
def verify_count_on_hand?
|
|
110
|
-
count_on_hand_changed? && !backorderable? &&
|
|
110
|
+
count_on_hand_changed? && !backorderable? && !variant.preorder? &&
|
|
111
|
+
(count_on_hand < count_on_hand_was) && (count_on_hand < 0)
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
# Process backorders based on amount of stock received
|
|
@@ -157,7 +157,10 @@ module Spree
|
|
|
157
157
|
else
|
|
158
158
|
on_hand = item.count_on_hand
|
|
159
159
|
on_hand = 0 if on_hand < 0
|
|
160
|
-
|
|
160
|
+
# A pre-order oversells like a backorder: the shortfall becomes
|
|
161
|
+
# backordered inventory units (the backorder_limit cap is enforced
|
|
162
|
+
# upstream by Stock::Quantifier#can_supply?).
|
|
163
|
+
backordered = item.backorderable? || variant.preorder? ? (quantity - on_hand) : 0
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
[on_hand, backordered]
|
data/app/models/spree/store.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Spree
|
|
|
28
28
|
#
|
|
29
29
|
TRANSLATABLE_FIELDS = %i[name meta_description meta_keywords seo_title customer_support_email
|
|
30
30
|
address contact_phone].freeze
|
|
31
|
-
translates(*TRANSLATABLE_FIELDS, column_fallback:
|
|
31
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback)
|
|
32
32
|
self::Translation.class_eval do
|
|
33
33
|
acts_as_paranoid
|
|
34
34
|
# deleted translation values still need to be accessible - remove deleted_at scope
|
|
@@ -46,7 +46,14 @@ module Spree
|
|
|
46
46
|
# email preferences
|
|
47
47
|
preference :send_consumer_transactional_emails, :boolean, default: true
|
|
48
48
|
# Checkout preferences
|
|
49
|
+
# Store-level fallback for the channel-owned `guest_checkout` preference
|
|
50
|
+
# (see Spree::Channel::Gating). Retained so existing accessors keep working.
|
|
49
51
|
preference :guest_checkout, :boolean, default: true
|
|
52
|
+
# Store-level fallback for the channel-owned `storefront_access` posture.
|
|
53
|
+
preference :storefront_access, :string, default: 'public'
|
|
54
|
+
# Canonical storefront origin used in customer-facing emails and links,
|
|
55
|
+
# e.g. "https://myshop.com" — see #storefront_url for the fallback chain.
|
|
56
|
+
preference :storefront_url, :string
|
|
50
57
|
preference :special_instructions_enabled, :boolean, default: false
|
|
51
58
|
preference :stock_reservation_ttl_minutes, :integer, default: 10
|
|
52
59
|
# Address preferences
|
|
@@ -74,8 +81,9 @@ module Spree
|
|
|
74
81
|
has_many :return_authorizations, through: :orders, class_name: 'Spree::ReturnAuthorization'
|
|
75
82
|
# has_many :reimbursements, through: :orders, class_name: 'Spree::Reimbursement' FIXME: we should fetch this via Customer Return
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
# :nullify (not :destroy) — clearing the collection must not cascade into
|
|
85
|
+
# Promotion#not_used? / payment records; orphaned rows are detached, not deleted.
|
|
86
|
+
has_many :payment_methods, class_name: 'Spree::PaymentMethod', dependent: :nullify
|
|
79
87
|
|
|
80
88
|
has_many :products, class_name: 'Spree::Product', dependent: :nullify
|
|
81
89
|
has_many :product_publications, through: :channels, source: :publications, class_name: 'Spree::ProductPublication'
|
|
@@ -91,11 +99,10 @@ module Spree
|
|
|
91
99
|
has_many :store_credit_events, through: :store_credits, class_name: 'Spree::StoreCreditEvent'
|
|
92
100
|
|
|
93
101
|
has_many :taxonomies, class_name: 'Spree::Taxonomy'
|
|
94
|
-
has_many :taxons,
|
|
95
|
-
has_many :categories,
|
|
102
|
+
has_many :taxons, class_name: 'Spree::Taxon'
|
|
103
|
+
has_many :categories, class_name: 'Spree::Category'
|
|
96
104
|
|
|
97
|
-
has_many :
|
|
98
|
-
has_many :promotions, through: :store_promotions, class_name: 'Spree::Promotion'
|
|
105
|
+
has_many :promotions, class_name: 'Spree::Promotion', dependent: :nullify
|
|
99
106
|
|
|
100
107
|
has_many :wishlists, class_name: 'Spree::Wishlist'
|
|
101
108
|
|
|
@@ -133,7 +140,10 @@ module Spree
|
|
|
133
140
|
validates :preferred_digital_asset_authorized_clicks, numericality: { only_integer: true, greater_than: 0 }
|
|
134
141
|
validates :preferred_digital_asset_authorized_days, numericality: { only_integer: true, greater_than: 0 }
|
|
135
142
|
validates :preferred_stock_reservation_ttl_minutes, numericality: { only_integer: true, greater_than: 0 }
|
|
143
|
+
validates :preferred_storefront_access, inclusion: { in: Spree::Channel::Gating::STOREFRONT_ACCESS }
|
|
144
|
+
validate :preferred_storefront_url_is_an_origin
|
|
136
145
|
validates :mail_from_address, email: { allow_blank: false }
|
|
146
|
+
validates :customer_support_email, email: { allow_blank: true }
|
|
137
147
|
# FIXME: we should remove this condition in v5
|
|
138
148
|
if !ENV['SPREE_DISABLE_DB_CONNECTION'] &&
|
|
139
149
|
connected? &&
|
|
@@ -152,6 +162,7 @@ module Spree
|
|
|
152
162
|
#
|
|
153
163
|
# Callbacks
|
|
154
164
|
before_validation :set_default_code, on: :create
|
|
165
|
+
before_validation :normalize_preferred_storefront_url
|
|
155
166
|
before_save :ensure_default_exists_and_is_unique
|
|
156
167
|
after_create :create_default_policies
|
|
157
168
|
|
|
@@ -276,11 +287,15 @@ module Spree
|
|
|
276
287
|
end
|
|
277
288
|
|
|
278
289
|
# Returns the storefront origin URL for use in customer-facing emails and links.
|
|
279
|
-
# Uses the
|
|
290
|
+
# Uses the `storefront_url` preference when set, then the oldest non-loopback
|
|
291
|
+
# allowed origin (the `http://localhost` origin seeded on install must never
|
|
292
|
+
# leak into customer emails), otherwise falls back to formatted_url.
|
|
280
293
|
#
|
|
281
294
|
# @return [String] e.g. "https://myshop.com"
|
|
282
295
|
def storefront_url
|
|
283
|
-
|
|
296
|
+
preferred_storefront_url.presence ||
|
|
297
|
+
allowed_origins.order(:created_at).reject(&:loopback?).first&.origin ||
|
|
298
|
+
formatted_url
|
|
284
299
|
end
|
|
285
300
|
|
|
286
301
|
# Returns true if the given URL's origin matches one of the store's allowed origins.
|
|
@@ -412,5 +427,26 @@ module Spree
|
|
|
412
427
|
def set_default_code
|
|
413
428
|
self.code = 'default' if code.blank?
|
|
414
429
|
end
|
|
430
|
+
|
|
431
|
+
# The storefront URL preference must always hold a canonical origin — it
|
|
432
|
+
# becomes the base for customer-email links and completes the storefront
|
|
433
|
+
# setup task, and the v3 Admin API writes it without any controller-side
|
|
434
|
+
# normalization. Parseable input is canonicalized here; garbage is left
|
|
435
|
+
# in place for the validation below to reject.
|
|
436
|
+
def normalize_preferred_storefront_url
|
|
437
|
+
raw = preferred_storefront_url
|
|
438
|
+
return if raw.blank?
|
|
439
|
+
|
|
440
|
+
normalized = Spree::AllowedOrigin.normalize_origin(raw)
|
|
441
|
+
self.preferred_storefront_url = normalized if normalized
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def preferred_storefront_url_is_an_origin
|
|
445
|
+
raw = preferred_storefront_url
|
|
446
|
+
return if raw.blank?
|
|
447
|
+
return if Spree::AllowedOrigin.normalize_origin(raw)
|
|
448
|
+
|
|
449
|
+
errors.add(:preferred_storefront_url, :invalid)
|
|
450
|
+
end
|
|
415
451
|
end
|
|
416
452
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Legacy join between Store and PaymentMethod. Superseded by the single-store
|
|
3
|
+
# +PaymentMethod#store+ FK in 5.6; retained only so the +spree_multi_store+
|
|
4
|
+
# extension can restore +has_many :stores+ and so the backfill task can read
|
|
5
|
+
# historic attachments. Dropped in 6.0.
|
|
2
6
|
class StorePaymentMethod < Spree.base_class
|
|
3
7
|
self.table_name = 'spree_payment_methods_stores'
|
|
4
8
|
|
|
5
9
|
belongs_to :store, class_name: 'Spree::Store', touch: true
|
|
6
|
-
belongs_to :payment_method, class_name: 'Spree::PaymentMethod', touch: true
|
|
10
|
+
belongs_to :payment_method, class_name: 'Spree::PaymentMethod', touch: true
|
|
7
11
|
|
|
8
12
|
validates :store, :payment_method, presence: true
|
|
9
13
|
validates :store_id, uniqueness: { scope: :payment_method_id }
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Legacy join between Store and Promotion. Superseded by the single-store
|
|
3
|
+
# +Promotion#store+ FK in 5.6; retained only so the +spree_multi_store+
|
|
4
|
+
# extension can restore +has_many :stores+ and so the backfill task can read
|
|
5
|
+
# historic attachments. Dropped in 6.0.
|
|
2
6
|
class StorePromotion < Spree.base_class
|
|
3
7
|
self.table_name = 'spree_promotions_stores'
|
|
4
8
|
|