spree_core 5.4.3 → 5.5.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 +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +2 -1
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +28 -5
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/export.rb +30 -3
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +6 -1
- data/app/models/spree/market.rb +32 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +27 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +83 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
data/app/models/spree/base.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
class Spree::Base < ApplicationRecord
|
|
2
2
|
include Spree::Preferences::Preferable
|
|
3
|
+
include Spree::PreferenceSchema
|
|
3
4
|
include Spree::RansackableAttributes
|
|
4
5
|
include Spree::TranslatableResourceScopes
|
|
5
6
|
include Spree::IntegrationsConcern
|
|
6
7
|
include Spree::Publishable
|
|
7
8
|
include Spree::PrefixedId
|
|
9
|
+
include Spree::TypedAssociations
|
|
8
10
|
|
|
9
11
|
after_initialize do
|
|
10
12
|
if has_attribute?(:preferences) && !preferences.nil?
|
|
@@ -57,6 +59,10 @@ class Spree::Base < ApplicationRecord
|
|
|
57
59
|
true
|
|
58
60
|
end
|
|
59
61
|
|
|
62
|
+
def mysql_adapter?
|
|
63
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
|
|
64
|
+
end
|
|
65
|
+
|
|
60
66
|
def self.json_api_columns
|
|
61
67
|
column_names.reject { |c| c.match(/_id$|id|preferences|(.*)password|(.*)token|(.*)api_key|^original_(.*)/) }
|
|
62
68
|
end
|
|
@@ -71,10 +77,21 @@ class Spree::Base < ApplicationRecord
|
|
|
71
77
|
column_names.reject { |c| skipped_attributes.include?(c.to_s) }
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
# Public-API shorthand for this class, used as the `type` value on the
|
|
81
|
+
# wire (e.g. `"currency"` for `Spree::Promotion::Rules::Currency`,
|
|
82
|
+
# `"flat_rate"` for `Spree::Calculator::FlatRate`). Defaults to the
|
|
83
|
+
# demodulized + underscored leaf; override on a subclass when the wire
|
|
84
|
+
# format should stay stable across class renames.
|
|
85
|
+
def self.api_type
|
|
75
86
|
to_s.demodulize.underscore
|
|
76
87
|
end
|
|
77
88
|
|
|
89
|
+
# Backwards-compatible alias for `.api_type`. Delegates so subclass
|
|
90
|
+
# overrides of `api_type` are honored.
|
|
91
|
+
def self.json_api_type
|
|
92
|
+
api_type
|
|
93
|
+
end
|
|
94
|
+
|
|
78
95
|
def self.to_tom_select_json
|
|
79
96
|
pluck(:name, :id).map do |name, id|
|
|
80
97
|
{
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Lightweight distribution surface within a Store: online storefront, POS,
|
|
3
|
+
# marketplace integration, wholesale portal. Channels carry order
|
|
4
|
+
# attribution and the routing-strategy override.
|
|
5
|
+
class Channel < Spree.base_class
|
|
6
|
+
DEFAULT_CODE = 'online'.freeze
|
|
7
|
+
|
|
8
|
+
has_prefix_id :ch
|
|
9
|
+
|
|
10
|
+
include Spree::SingleStoreResource
|
|
11
|
+
include Spree::Metafields
|
|
12
|
+
include Spree::Metadata
|
|
13
|
+
include Spree::OrderRouting::HasStrategyPreference
|
|
14
|
+
|
|
15
|
+
# Empty -> falls back to the Store-level preference.
|
|
16
|
+
preference :order_routing_strategy, :string, default: nil
|
|
17
|
+
|
|
18
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
19
|
+
|
|
20
|
+
has_many :orders, class_name: 'Spree::Order', inverse_of: :channel, dependent: :nullify
|
|
21
|
+
has_many :order_routing_rules, class_name: 'Spree::OrderRoutingRule', dependent: :destroy
|
|
22
|
+
has_many :publications, class_name: 'Spree::ProductPublication', dependent: :destroy
|
|
23
|
+
has_many :products, through: :publications, class_name: 'Spree::Product'
|
|
24
|
+
|
|
25
|
+
attribute :active, :boolean, default: true
|
|
26
|
+
|
|
27
|
+
# HTTP headers arrive as ASCII-8BIT; +parameterize+ raises on those.
|
|
28
|
+
normalizes :code, with: ->(value) { value.to_s.dup.force_encoding(Encoding::UTF_8).parameterize.presence }
|
|
29
|
+
|
|
30
|
+
before_validation :backfill_code_from_name, if: -> { code.blank? && name.present? }
|
|
31
|
+
before_validation :promote_first_channel_to_default
|
|
32
|
+
|
|
33
|
+
validates :name, :store, presence: true
|
|
34
|
+
validates :code, presence: true, uniqueness: { scope: spree_base_uniqueness_scope + [:store_id] }
|
|
35
|
+
|
|
36
|
+
# Demote any prior default in the same transaction so the partial unique
|
|
37
|
+
# index ("only one default per store") never sees two TRUE rows. Runs
|
|
38
|
+
# before save so MySQL — which can't enforce a partial unique index — also
|
|
39
|
+
# arrives at a single default without relying on DB constraints.
|
|
40
|
+
before_save :demote_other_defaults, if: -> { default? && will_save_change_to_default? }
|
|
41
|
+
before_destroy :ensure_not_default
|
|
42
|
+
after_create :ensure_default_order_routing_rules
|
|
43
|
+
|
|
44
|
+
scope :active, -> { where(active: true) }
|
|
45
|
+
scope :default, -> { where(default: true) }
|
|
46
|
+
|
|
47
|
+
self.whitelisted_ransackable_attributes = %w[name code active default store_id]
|
|
48
|
+
|
|
49
|
+
# Publishes the given products on this channel by creating/upserting ProductPublications.
|
|
50
|
+
# Optionally sets the publication window; if not given, the products will be published immediately
|
|
51
|
+
# with no end date.
|
|
52
|
+
# @param product_ids [Array<Integer>, Integer] the IDs of the products to publish on this channel
|
|
53
|
+
# @param published_at [Time, nil] when the publications go live; nil means immediately
|
|
54
|
+
# @param unpublished_at [Time, nil] when the publications come down; nil means never
|
|
55
|
+
# @return [Integer] the number of ProductPublications created or updated
|
|
56
|
+
def add_products(product_ids, published_at: nil, unpublished_at: nil)
|
|
57
|
+
product_ids = Array(product_ids).map(&:to_s).uniq
|
|
58
|
+
return 0 if product_ids.empty?
|
|
59
|
+
|
|
60
|
+
now = Time.current
|
|
61
|
+
# Only include window columns in the upsert payload when the caller
|
|
62
|
+
# explicitly passed a value. Leaving them out keeps existing
|
|
63
|
+
# publication schedules intact on re-publish — otherwise +on_duplicate:
|
|
64
|
+
# :update+ + +update_only+ would rewrite scheduled +published_at+ /
|
|
65
|
+
# +unpublished_at+ to NULL whenever the bulk action re-runs without
|
|
66
|
+
# dates.
|
|
67
|
+
base = { channel_id: id, created_at: now, updated_at: now }
|
|
68
|
+
base[:published_at] = published_at unless published_at.nil?
|
|
69
|
+
base[:unpublished_at] = unpublished_at unless unpublished_at.nil?
|
|
70
|
+
|
|
71
|
+
records_to_upsert = product_ids.map { |product_id| base.merge(product_id: product_id) }
|
|
72
|
+
|
|
73
|
+
# Only update the window columns the caller passed. When neither was
|
|
74
|
+
# passed, treat re-publish as a no-op (+on_duplicate: :skip+ → MySQL
|
|
75
|
+
# +INSERT IGNORE+, PG/SQLite +ON CONFLICT DO NOTHING+).
|
|
76
|
+
update_columns = []
|
|
77
|
+
update_columns << :published_at unless published_at.nil?
|
|
78
|
+
update_columns << :unpublished_at unless unpublished_at.nil?
|
|
79
|
+
opts = if update_columns.empty?
|
|
80
|
+
{ on_duplicate: :skip }
|
|
81
|
+
else
|
|
82
|
+
{ record_timestamps: false, update_only: update_columns, on_duplicate: :update }
|
|
83
|
+
end
|
|
84
|
+
# MySQL infers the conflict target from the table's unique constraints
|
|
85
|
+
# and rejects an explicit +unique_by+; PostgreSQL/SQLite require it.
|
|
86
|
+
opts[:unique_by] = %i[product_id channel_id] unless mysql_adapter?
|
|
87
|
+
|
|
88
|
+
Spree::ProductPublication.upsert_all(records_to_upsert, **opts)
|
|
89
|
+
|
|
90
|
+
products = Spree::Product.where(id: product_ids)
|
|
91
|
+
products.touch_all
|
|
92
|
+
products.each(&:enqueue_search_index)
|
|
93
|
+
touch
|
|
94
|
+
|
|
95
|
+
records_to_upsert.size
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Unpublishes the given products from this channel.
|
|
99
|
+
# @param product_ids [Array<Integer>, Integer] the IDs of the products to unpublish
|
|
100
|
+
# @return [Integer] the number of ProductPublications destroyed
|
|
101
|
+
def remove_products(product_ids)
|
|
102
|
+
product_ids = Array(product_ids).map(&:to_s).uniq
|
|
103
|
+
return 0 if product_ids.empty?
|
|
104
|
+
|
|
105
|
+
count = publications.where(product_id: product_ids).destroy_all.size
|
|
106
|
+
|
|
107
|
+
products = Spree::Product.where(id: product_ids)
|
|
108
|
+
products.touch_all
|
|
109
|
+
products.each(&:enqueue_search_index)
|
|
110
|
+
|
|
111
|
+
touch if count.positive?
|
|
112
|
+
count
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
def can_be_deleted?
|
|
117
|
+
!default?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def ensure_not_default
|
|
123
|
+
return if can_be_deleted?
|
|
124
|
+
return if destroyed_by_association.present?
|
|
125
|
+
|
|
126
|
+
errors.add(:base, Spree.t('errors.messages.cannot_delete_default_channel'))
|
|
127
|
+
throw :abort
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def backfill_code_from_name
|
|
131
|
+
self.code = name
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# First channel on a store becomes the default. Lets the
|
|
135
|
+
# +Stores::Channels#ensure_default_channel+ seed path and the legacy
|
|
136
|
+
# admin "create channel" form both produce a sensible default without
|
|
137
|
+
# the caller having to know.
|
|
138
|
+
def promote_first_channel_to_default
|
|
139
|
+
return if default
|
|
140
|
+
return unless new_record? && store_id.present?
|
|
141
|
+
|
|
142
|
+
self.default = true unless Spree::Channel.where(store_id: store_id).exists?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def demote_other_defaults
|
|
146
|
+
Spree::Channel.where(store_id: store_id, default: true).where.not(id: id).update_all(default: false)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Default ordering: preferred location wins, then minimize splits, then
|
|
150
|
+
# fall back to StockLocation.default. See docs/plans/6.0-order-routing.md.
|
|
151
|
+
def ensure_default_order_routing_rules
|
|
152
|
+
return if order_routing_rules.any?
|
|
153
|
+
|
|
154
|
+
Spree::OrderRouting::Rules::PreferredLocation.create!(store: store, channel: self, position: 1)
|
|
155
|
+
Spree::OrderRouting::Rules::MinimizeSplits.create!(store: store , channel: self, position: 2)
|
|
156
|
+
Spree::OrderRouting::Rules::DefaultLocation.create!(store: store, channel: self, position: 3)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/app/models/spree/country.rb
CHANGED
data/app/models/spree/current.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Spree
|
|
|
4
4
|
# All attributes are automatically reset between requests by Rails.
|
|
5
5
|
# Fallback chains ensure sensible defaults when attributes are not explicitly set.
|
|
6
6
|
class Current < ::ActiveSupport::CurrentAttributes
|
|
7
|
-
attribute :store, :market, :currency, :locale, :zone, :default_tax_zone, :price_lists, :global_pricing_context
|
|
7
|
+
attribute :store, :channel, :market, :currency, :locale, :zone, :default_tax_zone, :price_lists, :global_pricing_context
|
|
8
8
|
|
|
9
9
|
resets { @default_tax_zone_loaded = false }
|
|
10
10
|
|
|
@@ -14,6 +14,10 @@ module Spree
|
|
|
14
14
|
super || Spree::Store.default
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def channel
|
|
18
|
+
super || (self.channel = store&.default_channel)
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
# Returns the current market, falling back to the store's default market.
|
|
18
22
|
# @return [Spree::Market, nil]
|
|
19
23
|
def market
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Constant alias for the legacy Spree::Metafield class. Lets controllers,
|
|
3
|
+
# serializers, and 5.5+ extensions reference the model by its 6.0-bound name
|
|
4
|
+
# without the actual class rename (which lands with the table rename in 6.0).
|
|
5
|
+
#
|
|
6
|
+
# This is a true constant alias — the underlying class, table, STI subclasses,
|
|
7
|
+
# and `model_name` are all `Spree::Metafield`. Only the constant name differs.
|
|
8
|
+
CustomField = Metafield
|
|
9
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Constant alias for the legacy Spree::MetafieldDefinition class. Lets
|
|
3
|
+
# controllers, serializers, and 5.5+ extensions reference the model by its
|
|
4
|
+
# 6.0-bound name without the actual class rename (which lands with the table
|
|
5
|
+
# rename in 6.0).
|
|
6
|
+
CustomFieldDefinition = MetafieldDefinition
|
|
7
|
+
end
|
|
@@ -10,6 +10,11 @@ module Spree
|
|
|
10
10
|
belongs_to :store, class_name: 'Spree::Store', inverse_of: :customer_groups
|
|
11
11
|
has_many :customer_group_users, class_name: 'Spree::CustomerGroupUser', dependent: :destroy
|
|
12
12
|
has_many :users, through: :customer_group_users, source: :user, source_type: Spree.user_class.to_s
|
|
13
|
+
# `customers` is the public name across the v3 API; declaring it as its
|
|
14
|
+
# own association (rather than `alias_method`) is what lets `customer_ids=`
|
|
15
|
+
# exist and what makes the `PrefixedId` auto-decoder in `assign_attributes`
|
|
16
|
+
# recognise the key — that lookup keys off `reflect_on_association(:customers)`.
|
|
17
|
+
has_many :customers, through: :customer_group_users, source: :user, source_type: Spree.user_class.to_s
|
|
13
18
|
|
|
14
19
|
#
|
|
15
20
|
# Validations
|
|
@@ -25,9 +30,10 @@ module Spree
|
|
|
25
30
|
#
|
|
26
31
|
# Instance Methods
|
|
27
32
|
#
|
|
28
|
-
def
|
|
29
|
-
customer_group_users.
|
|
33
|
+
def customers_count
|
|
34
|
+
customer_group_users.size
|
|
30
35
|
end
|
|
36
|
+
alias_method :users_count, :customers_count
|
|
31
37
|
|
|
32
38
|
# Bulk add customers to the group
|
|
33
39
|
# @param user_ids [Array] array of user IDs to add
|
data/app/models/spree/export.rb
CHANGED
|
@@ -22,13 +22,16 @@ module Spree
|
|
|
22
22
|
# Associations
|
|
23
23
|
#
|
|
24
24
|
belongs_to :store, class_name: 'Spree::Store'
|
|
25
|
-
|
|
25
|
+
# Optional so secret-API-key callers (apps / server-to-server) can create
|
|
26
|
+
# exports without a human user attached. The email notification is
|
|
27
|
+
# skipped for these — apps poll instead.
|
|
28
|
+
belongs_to :user, class_name: Spree.admin_user_class.to_s, optional: true
|
|
26
29
|
belongs_to :vendor, -> { with_deleted }, class_name: 'Spree::Vendor', optional: true
|
|
27
30
|
|
|
28
31
|
#
|
|
29
32
|
# Validations
|
|
30
33
|
#
|
|
31
|
-
validates :format, :store, :
|
|
34
|
+
validates :format, :store, :type, presence: true
|
|
32
35
|
|
|
33
36
|
#
|
|
34
37
|
# Enums
|
|
@@ -127,12 +130,34 @@ module Spree
|
|
|
127
130
|
|
|
128
131
|
def records_to_export
|
|
129
132
|
if search_params.present?
|
|
130
|
-
|
|
133
|
+
params = search_params.is_a?(String) ? JSON.parse(search_params.to_s).to_h : search_params
|
|
134
|
+
scope.ransack(decode_prefixed_id_filters(params))
|
|
131
135
|
else
|
|
132
136
|
scope.ransack
|
|
133
137
|
end.result
|
|
134
138
|
end
|
|
135
139
|
|
|
140
|
+
# Replace any prefixed IDs in `search_params` with their raw DB IDs so
|
|
141
|
+
# Ransack can match them. Without this, an admin filtering an export by
|
|
142
|
+
# a foreign key (`promotion_id_eq: 'promo_xxx'`, `vendor_id_in: [...]`)
|
|
143
|
+
# would always get zero rows. We only touch values that look like
|
|
144
|
+
# prefixed IDs — anything else (numeric IDs, code strings, ranges,
|
|
145
|
+
# state names) passes through untouched.
|
|
146
|
+
def decode_prefixed_id_filters(params)
|
|
147
|
+
params.transform_values { |value| decode_search_value(value) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def decode_search_value(value)
|
|
151
|
+
case value
|
|
152
|
+
when String
|
|
153
|
+
Spree::PrefixedId.prefixed_id?(value) ? (Spree::PrefixedId.decode_prefixed_id(value) || value) : value
|
|
154
|
+
when Array
|
|
155
|
+
value.map { |v| decode_search_value(v) }
|
|
156
|
+
else
|
|
157
|
+
value
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
136
161
|
def scope_includes
|
|
137
162
|
[]
|
|
138
163
|
end
|
|
@@ -181,6 +206,8 @@ module Spree
|
|
|
181
206
|
end
|
|
182
207
|
|
|
183
208
|
def send_export_done_email
|
|
209
|
+
return if user.blank? # App-created exports (secret API key) have no user to email.
|
|
210
|
+
|
|
184
211
|
Spree::ExportMailer.export_done(self).deliver_later
|
|
185
212
|
end
|
|
186
213
|
|
data/app/models/spree/gateway.rb
CHANGED
|
@@ -6,6 +6,31 @@ module Spree
|
|
|
6
6
|
|
|
7
7
|
validates :type, presence: true, inclusion: { in: :valid_providers_list }
|
|
8
8
|
|
|
9
|
+
# Payment provider gems conventionally ship a top-level `Gateway`
|
|
10
|
+
# class — `SpreeStripe::Gateway`, `SpreeAdyen::Gateway`,
|
|
11
|
+
# `SpreePaypalCheckout::Gateway`. The default demodulized
|
|
12
|
+
# `api_type` collapses every provider to `"gateway"`, which
|
|
13
|
+
# collides in the registry and produces duplicate keys in admin
|
|
14
|
+
# UIs. For gateway subclasses, use the outer module instead (with
|
|
15
|
+
# a leading `Spree` namespace stripped), matching the labelling
|
|
16
|
+
# convention in `PreferenceSchema#subclass_label`:
|
|
17
|
+
#
|
|
18
|
+
# SpreeStripe::Gateway → "stripe"
|
|
19
|
+
# SpreeAdyen::Gateway → "adyen"
|
|
20
|
+
# SpreePaypalCheckout::Gateway → "paypal_checkout"
|
|
21
|
+
# MyShop::Gateway → "my_shop"
|
|
22
|
+
#
|
|
23
|
+
# Subclasses nested under a Gateway module (e.g.
|
|
24
|
+
# `Spree::Gateway::Bogus`) demodulize to their own leaf and
|
|
25
|
+
# bypass this fallback.
|
|
26
|
+
def self.api_type
|
|
27
|
+
leaf = super
|
|
28
|
+
return leaf unless leaf == 'gateway'
|
|
29
|
+
|
|
30
|
+
outer = to_s.deconstantize.delete_prefix('Spree::').delete_prefix('Spree').presence
|
|
31
|
+
outer ? outer.underscore : leaf
|
|
32
|
+
end
|
|
33
|
+
|
|
9
34
|
def payment_source_class
|
|
10
35
|
CreditCard
|
|
11
36
|
end
|
|
@@ -62,7 +62,7 @@ module Spree
|
|
|
62
62
|
#
|
|
63
63
|
# Ransack
|
|
64
64
|
#
|
|
65
|
-
self.whitelisted_ransackable_attributes = %w[code user_id state]
|
|
65
|
+
self.whitelisted_ransackable_attributes = %w[code user_id state gift_card_batch_id created_by_id]
|
|
66
66
|
self.whitelisted_ransackable_associations = %w[users orders batch]
|
|
67
67
|
self.whitelisted_ransackable_scopes = %w[active expired redeemed partially_redeemed]
|
|
68
68
|
|
|
@@ -18,7 +18,10 @@ module Spree
|
|
|
18
18
|
# Validations
|
|
19
19
|
#
|
|
20
20
|
validates :codes_count, :amount, :prefix, presence: true
|
|
21
|
-
validates :codes_count, numericality: {
|
|
21
|
+
validates :codes_count, numericality: {
|
|
22
|
+
greater_than: 0,
|
|
23
|
+
less_than_or_equal_to: ->(_record) { Spree::Config[:gift_card_batch_limit].to_i }
|
|
24
|
+
}
|
|
22
25
|
validates :store, :currency, presence: true
|
|
23
26
|
validates :amount, numericality: { greater_than: 0 }
|
|
24
27
|
|
data/app/models/spree/import.rb
CHANGED
|
@@ -62,6 +62,7 @@ module Spree
|
|
|
62
62
|
event :complete do
|
|
63
63
|
transition from: :processing, to: :completed
|
|
64
64
|
end
|
|
65
|
+
after_transition to: :completed, do: :touch_store
|
|
65
66
|
after_transition to: :completed, do: :publish_import_completed_event
|
|
66
67
|
# NOTE: send_import_completed_email and update_loader_in_import_view
|
|
67
68
|
# are now handled by Spree::Admin::ImportSubscriber listening to 'import.completed' event
|
|
@@ -181,6 +182,10 @@ module Spree
|
|
|
181
182
|
"#{Spree.t(type.demodulize.pluralize.downcase)} #{number}"
|
|
182
183
|
end
|
|
183
184
|
|
|
185
|
+
def touch_store
|
|
186
|
+
store.touch
|
|
187
|
+
end
|
|
188
|
+
|
|
184
189
|
def publish_import_completed_event
|
|
185
190
|
publish_event('import.completed')
|
|
186
191
|
end
|
|
@@ -41,6 +41,11 @@ module Spree
|
|
|
41
41
|
# are now handled by Spree::Admin::ImportRowSubscriber
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# How long a row may sit in `processing` before we consider its owning worker dead
|
|
45
|
+
# (OOM, SIGKILL, deploy without graceful drain — none of these trigger a Sidekiq
|
|
46
|
+
# retry). After this window, stalled rows no longer block the import from completing.
|
|
47
|
+
STALLED_PROCESSING_AFTER = 1.hour
|
|
48
|
+
|
|
44
49
|
#
|
|
45
50
|
# Scopes
|
|
46
51
|
#
|
|
@@ -48,6 +53,13 @@ module Spree
|
|
|
48
53
|
scope :completed, -> { where(status: :completed) }
|
|
49
54
|
scope :failed, -> { where(status: :failed) }
|
|
50
55
|
scope :processed, -> { where(status: %i[completed failed]) }
|
|
56
|
+
# Rows still legitimately blocking import completion: `pending` (not started) or
|
|
57
|
+
# `processing` with a recent updated_at (worker still alive). Orphaned `processing`
|
|
58
|
+
# rows past the stall window are excluded so a dead worker can't permanently block
|
|
59
|
+
# completion — operators can clean those up separately.
|
|
60
|
+
scope :in_flight, -> {
|
|
61
|
+
where(status: :pending).or(where(status: :processing).where(updated_at: STALLED_PROCESSING_AFTER.ago..))
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
def data_json
|
|
53
65
|
@data_json ||= JSON.parse(data)
|
|
@@ -24,6 +24,7 @@ module Spree
|
|
|
24
24
|
has_many :inventory_units, class_name: 'Spree::InventoryUnit', inverse_of: :line_item, dependent: :destroy
|
|
25
25
|
has_many :shipments, through: :inventory_units, source: :shipment
|
|
26
26
|
has_many :digital_links, dependent: :destroy
|
|
27
|
+
has_many :stock_reservations, class_name: 'Spree::StockReservation', inverse_of: :line_item, dependent: :destroy
|
|
27
28
|
|
|
28
29
|
before_validation :copy_price
|
|
29
30
|
before_validation :copy_tax_category
|
|
@@ -164,9 +165,13 @@ module Spree
|
|
|
164
165
|
|
|
165
166
|
# Returns true if the line item has sufficient stock
|
|
166
167
|
#
|
|
168
|
+
# The order's own active stock reservations are excluded from the
|
|
169
|
+
# availability check — a customer's own checkout hold must not make
|
|
170
|
+
# their own line item look out of stock.
|
|
171
|
+
#
|
|
167
172
|
# @return [Boolean]
|
|
168
173
|
def sufficient_stock?
|
|
169
|
-
can_supply?
|
|
174
|
+
Spree::Stock::Quantifier.new(variant, excluded_order: order).can_supply?(quantity)
|
|
170
175
|
end
|
|
171
176
|
|
|
172
177
|
# Returns true if the line item has insufficient stock
|
data/app/models/spree/market.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Spree
|
|
|
10
10
|
#
|
|
11
11
|
# Associations
|
|
12
12
|
#
|
|
13
|
-
belongs_to :store, class_name: 'Spree::Store', touch: true
|
|
13
|
+
belongs_to :store, class_name: 'Spree::Store', touch: true, inverse_of: :markets
|
|
14
14
|
has_many :market_countries, class_name: 'Spree::MarketCountry', dependent: :destroy
|
|
15
15
|
has_many :countries, through: :market_countries, class_name: 'Spree::Country'
|
|
16
16
|
has_many :orders, class_name: 'Spree::Order', dependent: :nullify
|
|
@@ -81,6 +81,37 @@ module Spree
|
|
|
81
81
|
@supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact.uniq.sort
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Accepts an Array of locale codes and persists them as a comma-separated
|
|
85
|
+
# string on the `supported_locales` column. Strings are still accepted
|
|
86
|
+
# verbatim so legacy callers (the Rails admin form, raw seed scripts)
|
|
87
|
+
# keep working.
|
|
88
|
+
#
|
|
89
|
+
# @param value [Array<String>, String, nil]
|
|
90
|
+
def supported_locales=(value)
|
|
91
|
+
@supported_locales_list = nil
|
|
92
|
+
normalized = value.is_a?(Array) ? value.compact.uniq.reject(&:blank?).join(',') : value
|
|
93
|
+
super(normalized)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Read companion for `country_isos=`. Returns the sorted list of ISO codes
|
|
97
|
+
# currently assigned to the market.
|
|
98
|
+
#
|
|
99
|
+
# @return [Array<String>]
|
|
100
|
+
def country_isos
|
|
101
|
+
countries.map(&:iso).compact.sort
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Accepts an Array of 2-letter ISO codes and resolves them to the matching
|
|
105
|
+
# `Spree::Country` records, replacing the market's countries. Unknown codes
|
|
106
|
+
# are silently dropped — the `validates :countries, presence: true` covers
|
|
107
|
+
# the "every ISO was bogus" case.
|
|
108
|
+
#
|
|
109
|
+
# @param values [Array<String>]
|
|
110
|
+
def country_isos=(values)
|
|
111
|
+
isos = Array(values).compact.map { |v| v.to_s.upcase }.reject(&:blank?)
|
|
112
|
+
self.countries = isos.any? ? Spree::Country.where(iso: isos) : []
|
|
113
|
+
end
|
|
114
|
+
|
|
84
115
|
# Returns true when the market is safe to delete. A market cannot be deleted
|
|
85
116
|
# if it is the default market or the only market in the store, since
|
|
86
117
|
# Spree::Current.currency would have no fallback.
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class Metafield < Spree.base_class
|
|
3
|
+
# Map of API-facing tokens to Ruby STI class names. The wire format is the
|
|
4
|
+
# token (`short_text`); the database column stores the class name. Reads
|
|
5
|
+
# translate to the token via `field_type`; writes accept either form.
|
|
6
|
+
# Plugin-defined types fall through to the raw class name until 6.0 when a
|
|
7
|
+
# registration API lands.
|
|
8
|
+
TYPE_TOKENS = {
|
|
9
|
+
'short_text' => 'Spree::Metafields::ShortText',
|
|
10
|
+
'long_text' => 'Spree::Metafields::LongText',
|
|
11
|
+
'rich_text' => 'Spree::Metafields::RichText',
|
|
12
|
+
'number' => 'Spree::Metafields::Number',
|
|
13
|
+
'boolean' => 'Spree::Metafields::Boolean',
|
|
14
|
+
'json' => 'Spree::Metafields::Json'
|
|
15
|
+
}.freeze
|
|
16
|
+
TYPE_CLASS_TO_TOKEN = TYPE_TOKENS.invert.freeze
|
|
17
|
+
|
|
18
|
+
# Array form consumed by serializers via
|
|
19
|
+
# `typelize field_type: Spree::Metafield::FIELD_TYPE_TOKENS`. Typelizer
|
|
20
|
+
# emits a string-literal union in TypeScript and `{type: string, enum: […]}`
|
|
21
|
+
# in OpenAPI (string-array form was added in typelizer 0.10.0).
|
|
22
|
+
FIELD_TYPE_TOKENS = TYPE_TOKENS.keys.freeze
|
|
23
|
+
|
|
3
24
|
has_prefix_id :cf
|
|
4
25
|
|
|
5
26
|
#
|
|
@@ -8,6 +29,21 @@ module Spree
|
|
|
8
29
|
belongs_to :resource, polymorphic: true, touch: true
|
|
9
30
|
belongs_to :metafield_definition, class_name: 'Spree::MetafieldDefinition'
|
|
10
31
|
|
|
32
|
+
#
|
|
33
|
+
# API naming bridge — internal column rename lands in 6.0
|
|
34
|
+
#
|
|
35
|
+
alias_attribute :custom_field_definition_id, :metafield_definition_id
|
|
36
|
+
|
|
37
|
+
# API-facing form of the STI `type` column. Returns the token
|
|
38
|
+
# (`short_text`) when the row's type is a registered built-in; falls
|
|
39
|
+
# through to the raw class name for plugin types.
|
|
40
|
+
#
|
|
41
|
+
# `self[:type]` reads the raw column to bypass AR's STI reader (which
|
|
42
|
+
# returns the resolved class constant, not a string).
|
|
43
|
+
def field_type
|
|
44
|
+
TYPE_CLASS_TO_TOKEN[self[:type]] || self[:type]
|
|
45
|
+
end
|
|
46
|
+
|
|
11
47
|
#
|
|
12
48
|
# Delegations
|
|
13
49
|
#
|
|
@@ -43,6 +79,8 @@ module Spree
|
|
|
43
79
|
private
|
|
44
80
|
|
|
45
81
|
def set_type_from_metafield_definition
|
|
82
|
+
return if metafield_definition.blank?
|
|
83
|
+
|
|
46
84
|
self.type ||= metafield_definition.metafield_type
|
|
47
85
|
end
|
|
48
86
|
|
|
@@ -18,6 +18,7 @@ module Spree
|
|
|
18
18
|
validates :metafield_type, presence: true, inclusion: { in: :valid_available_types }
|
|
19
19
|
validates :resource_type, presence: true, inclusion: { in: :valid_available_resources }
|
|
20
20
|
validates :key, uniqueness: { scope: spree_base_uniqueness_scope + [:resource_type, :namespace] }
|
|
21
|
+
validate :field_type_input_must_be_recognized
|
|
21
22
|
|
|
22
23
|
#
|
|
23
24
|
# Scopes
|
|
@@ -51,14 +52,29 @@ module Spree
|
|
|
51
52
|
self.whitelisted_ransackable_attributes = %w[key namespace name resource_type display_on]
|
|
52
53
|
self.whitelisted_ransackable_scopes = %w[search multi_search]
|
|
53
54
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# API naming bridge — internal columns rename in 6.0. `label` matches
|
|
56
|
+
# OptionType/OptionValue conventions. (`storefront_visible` lives on
|
|
57
|
+
# the `Spree::DisplayOn` concern, shared with PaymentMethod + ShippingMethod —
|
|
58
|
+
# see docs/plans/5.5-6.0-display-on-to-boolean.md.)
|
|
59
|
+
alias_attribute :label, :name
|
|
60
|
+
|
|
61
|
+
# API-facing token for the STI subclass name stored in `metafield_type`.
|
|
62
|
+
# Reader returns the registered token (`short_text`); writer accepts either
|
|
63
|
+
# the token or the legacy class-name form for back-compat.
|
|
64
|
+
def field_type
|
|
65
|
+
Spree::Metafield::TYPE_CLASS_TO_TOKEN[metafield_type] || metafield_type
|
|
58
66
|
end
|
|
59
67
|
|
|
60
|
-
def
|
|
61
|
-
|
|
68
|
+
def field_type=(value)
|
|
69
|
+
v = value.to_s
|
|
70
|
+
mapped = Spree::Metafield::TYPE_TOKENS[v]
|
|
71
|
+
# An input is "recognized" when it's either a known token (mapped to a
|
|
72
|
+
# class) or already a known class name. Anything else gets surfaced as
|
|
73
|
+
# an error on `field_type` so API clients get a token-vocabulary
|
|
74
|
+
# message instead of the raw class-name inclusion error on
|
|
75
|
+
# `metafield_type`.
|
|
76
|
+
@field_type_input_recognized = !mapped.nil? || Spree::Metafield::TYPE_CLASS_TO_TOKEN.key?(v)
|
|
77
|
+
self.metafield_type = mapped || value
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
# Returns the full key with namespace
|
|
@@ -91,6 +107,13 @@ module Spree
|
|
|
91
107
|
self.class.available_types.map(&:to_s)
|
|
92
108
|
end
|
|
93
109
|
|
|
110
|
+
def field_type_input_must_be_recognized
|
|
111
|
+
return if @field_type_input_recognized.nil? || @field_type_input_recognized
|
|
112
|
+
|
|
113
|
+
tokens = Spree::Metafield::TYPE_TOKENS.keys.join(', ')
|
|
114
|
+
errors.add(:field_type, "is not a known custom field type (expected one of: #{tokens})")
|
|
115
|
+
end
|
|
116
|
+
|
|
94
117
|
def valid_available_resources
|
|
95
118
|
self.class.available_resources.map(&:to_s)
|
|
96
119
|
end
|
|
@@ -3,6 +3,16 @@ module Spree
|
|
|
3
3
|
class Json < Spree::Metafield
|
|
4
4
|
validate :value_must_be_valid_json
|
|
5
5
|
|
|
6
|
+
# Accept either a JSON-serialized String (from CSV / Admin UI text
|
|
7
|
+
# input) or a raw Hash / Array (from API callers that ship parsed
|
|
8
|
+
# objects). Non-String inputs get JSON-serialized so the underlying
|
|
9
|
+
# text column always holds canonical JSON.
|
|
10
|
+
#
|
|
11
|
+
# @param raw [String, Hash, Array, nil]
|
|
12
|
+
def value=(raw)
|
|
13
|
+
super(raw.is_a?(Hash) || raw.is_a?(Array) ? raw.to_json : raw)
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
def serialize_value
|
|
7
17
|
JSON.parse(value)
|
|
8
18
|
rescue JSON::ParserError
|