spree_core 6.0.0.rc1 → 6.0.0.rc3
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/README.md +3 -3
- data/app/jobs/spree/search_provider/index_job.rb +4 -0
- data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
- data/app/mailers/spree/base_mailer.rb +4 -3
- data/app/models/concerns/spree/admin_user_methods.rb +2 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
- data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
- data/app/models/concerns/spree/calculator/tiers.rb +1 -14
- data/app/models/concerns/spree/canonical_decimals.rb +61 -0
- data/app/models/concerns/spree/customer_methods.rb +8 -4
- data/app/models/concerns/spree/derived_number.rb +11 -13
- data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
- data/app/models/concerns/spree/preference_schema.rb +59 -21
- data/app/models/concerns/spree/product_scopes.rb +5 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/purchase_order.rb +7 -2
- data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
- data/app/models/concerns/spree/purchase/totals.rb +1 -8
- data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
- data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
- data/app/models/concerns/spree/stores/email_branding.rb +2 -1
- data/app/models/concerns/spree/stores/markets.rb +5 -2
- data/app/models/concerns/spree/typed_adjustment_line.rb +4 -5
- data/app/models/concerns/spree/typed_associations.rb +6 -18
- data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
- data/app/models/spree/address.rb +1 -1
- data/app/models/spree/adjusters/largest_remainder.rb +14 -1
- data/app/models/spree/adjusters/promotion.rb +3 -4
- data/app/models/spree/allowed_origin.rb +3 -0
- data/app/models/spree/base.rb +29 -0
- data/app/models/spree/calculator/flat_percent_item_total.rb +1 -1
- data/app/models/spree/calculator/flat_rate.rb +2 -2
- data/app/models/spree/calculator/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
- data/app/models/spree/calculator/price_sack.rb +4 -4
- data/app/models/spree/calculator/shipping/digital_delivery.rb +2 -2
- data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +6 -4
- data/app/models/spree/calculator/shipping/flat_rate.rb +4 -4
- data/app/models/spree/calculator/shipping/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/shipping/per_item.rb +2 -2
- data/app/models/spree/calculator/shipping/price_sack.rb +4 -4
- data/app/models/spree/calculator/tiered_flat_rate.rb +2 -2
- data/app/models/spree/calculator/tiered_percent.rb +1 -1
- data/app/models/spree/catalog.rb +1 -0
- data/app/models/spree/catalog_price.rb +0 -7
- data/app/models/spree/catalog_price_tier.rb +0 -8
- data/app/models/spree/category.rb +3 -1
- data/app/models/spree/channel/gating.rb +2 -1
- data/app/models/spree/channel.rb +2 -0
- data/app/models/spree/claim.rb +1 -0
- data/app/models/spree/collection.rb +2 -0
- data/app/models/spree/commission_rate.rb +5 -1
- data/app/models/spree/commission_rules/category_rule.rb +2 -5
- data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
- data/app/models/spree/commission_rules/seller_rule.rb +2 -5
- data/app/models/spree/company.rb +1 -0
- data/app/models/spree/custom_field_definition.rb +11 -12
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/data_request.rb +1 -0
- data/app/models/spree/delivery_method.rb +6 -1
- data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
- data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
- data/app/models/spree/delivery_profile.rb +1 -0
- data/app/models/spree/delivery_rate.rb +3 -3
- data/app/models/spree/delivery_zone.rb +1 -1
- data/app/models/spree/exchange.rb +1 -0
- data/app/models/spree/exports/report.rb +2 -2
- data/app/models/spree/file_upload.rb +155 -0
- data/app/models/spree/fulfillment.rb +13 -3
- data/app/models/spree/fulfillment_group.rb +6 -1
- data/app/models/spree/gateway/bogus.rb +2 -0
- data/app/models/spree/gateway.rb +1 -1
- data/app/models/spree/gift_card.rb +6 -7
- data/app/models/spree/gift_card_batch.rb +3 -4
- data/app/models/spree/import.rb +3 -0
- data/app/models/spree/integration.rb +1 -1
- data/app/models/spree/line_item.rb +2 -2
- data/app/models/spree/market.rb +7 -2
- data/app/models/spree/media.rb +1 -9
- data/app/models/spree/newsletter_subscriber.rb +1 -0
- data/app/models/spree/option_type.rb +2 -1
- data/app/models/spree/option_value.rb +2 -1
- data/app/models/spree/order.rb +5 -5
- data/app/models/spree/order_group.rb +7 -2
- data/app/models/spree/package_type.rb +1 -0
- data/app/models/spree/payment/gateway_options.rb +1 -1
- data/app/models/spree/payment.rb +3 -10
- data/app/models/spree/payment_method/check.rb +2 -0
- data/app/models/spree/payment_method/store_credit.rb +11 -9
- data/app/models/spree/payment_method.rb +42 -2
- data/app/models/spree/policy.rb +1 -0
- data/app/models/spree/price.rb +17 -14
- data/app/models/spree/price_adjustment_tier.rb +4 -0
- data/app/models/spree/price_list.rb +13 -7
- data/app/models/spree/price_rules/channel_rule.rb +2 -5
- data/app/models/spree/price_rules/customer_group_rule.rb +2 -5
- data/app/models/spree/price_rules/market_rule.rb +2 -5
- data/app/models/spree/price_rules/user_rule.rb +1 -3
- data/app/models/spree/product/channels.rb +9 -2
- data/app/models/spree/product.rb +23 -23
- data/app/models/spree/product_type.rb +1 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +1 -1
- data/app/models/spree/promotion/rules/category.rb +1 -1
- data/app/models/spree/promotion/rules/channel.rb +2 -5
- data/app/models/spree/promotion/rules/country.rb +3 -4
- data/app/models/spree/promotion/rules/currency.rb +1 -1
- data/app/models/spree/promotion/rules/customer_group.rb +2 -5
- data/app/models/spree/promotion/rules/item_total.rb +2 -2
- data/app/models/spree/promotion/rules/market.rb +2 -5
- data/app/models/spree/promotion/rules/option_value.rb +2 -3
- data/app/models/spree/promotion/rules/product.rb +1 -1
- data/app/models/spree/promotion.rb +1 -0
- data/app/models/spree/purchase_order.rb +2 -0
- data/app/models/spree/refund.rb +13 -15
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/saved_report.rb +1 -0
- data/app/models/spree/search_provider/base.rb +4 -0
- data/app/models/spree/seller.rb +8 -1
- data/app/models/spree/seller_payout.rb +1 -0
- data/app/models/spree/seller_requirement.rb +1 -0
- data/app/models/spree/seller_requirement_submission.rb +1 -0
- data/app/models/spree/seller_transfer.rb +1 -0
- data/app/models/spree/shipping_label.rb +1 -0
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock_level.rb +2 -0
- data/app/models/spree/stock_location.rb +1 -0
- data/app/models/spree/stock_transfer.rb +2 -0
- data/app/models/spree/store.rb +37 -12
- data/app/models/spree/store_credit.rb +5 -4
- data/app/models/spree/supplier.rb +1 -0
- data/app/models/spree/tax_category.rb +1 -0
- data/app/models/spree/tax_exemption_certificate.rb +1 -0
- data/app/models/spree/tax_provider/internal.rb +12 -9
- data/app/models/spree/tax_rate.rb +29 -6
- data/app/models/spree/taxon.rb +1 -1
- data/app/models/spree/update_status.rb +25 -0
- data/app/models/spree/variant.rb +6 -9
- data/app/models/spree/webhook_delivery.rb +1 -0
- data/app/models/spree/webhook_endpoint.rb +36 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
- data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
- data/app/presenters/spree/csv/product_variant_presenter.rb +17 -8
- data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
- data/app/services/spree/carts/update.rb +3 -3
- data/app/services/spree/imports/row_processors/product_variant.rb +12 -0
- data/app/services/spree/locales.rb +39 -5
- data/app/services/spree/orders/create.rb +3 -3
- data/app/services/spree/orders/discounts/create.rb +8 -8
- data/app/services/spree/prices/bulk_upsert.rb +32 -25
- data/app/services/spree/seeds/stores.rb +1 -1
- data/app/services/spree/stores/provision_defaults.rb +13 -16
- data/app/services/spree/update_check.rb +160 -0
- data/app/services/spree/uploads/assign_stores.rb +92 -0
- data/app/validators/spree/attachment_content_type_validator.rb +19 -2
- data/app/validators/spree/locale_codes_validator.rb +22 -0
- data/app/workflows/spree/fulfillments/create.rb +2 -2
- data/app/workflows/spree/payments/capture.rb +20 -5
- data/app/workflows/spree/payments/process.rb +3 -1
- data/app/workflows/spree/price_lists/update.rb +4 -4
- data/app/workflows/spree/purchase_orders/create.rb +3 -1
- data/app/workflows/spree/shipping_labels/record.rb +2 -2
- data/app/workflows/spree/stock_transfers/create.rb +3 -1
- data/config/currency_exponents.json +180 -0
- data/config/locales/content_locales.rb +9 -0
- data/config/locales/en.yml +10 -2
- data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
- data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
- data/lib/generators/spree/dummy/templates/rails/test.rb +21 -2
- data/lib/spree/core/configuration.rb +18 -0
- data/lib/spree/core/engine.rb +26 -0
- data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
- data/lib/spree/core/preferences/json_conversion.rb +5 -1
- data/lib/spree/core/preferences/masking.rb +51 -1
- data/lib/spree/core/preferences/preferable.rb +212 -26
- data/lib/spree/core/preferences/preferable_class_methods.rb +121 -4
- data/lib/spree/core/ransack_configuration.rb +13 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +6 -3
- data/lib/spree/iso_data.rb +7 -0
- data/lib/spree/localized_number.rb +6 -0
- data/lib/spree/migrations.rb +4 -1
- data/lib/spree/money.rb +196 -15
- data/lib/spree/reporting/adapters/base.rb +7 -6
- data/lib/spree/reporting/registry.rb +13 -0
- data/lib/spree/testing_support/factories/product_factory.rb +9 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +22 -1
- data/lib/spree/uploads.rb +101 -0
- data/lib/tasks/markets.rake +2 -1
- data/lib/tasks/migrate_users_to_customers.rake +23 -18
- data/lib/tasks/money.rake +46 -0
- data/lib/tasks/seller_sample_data.rake +2 -2
- data/lib/tasks/store_settings.rake +13 -6
- data/lib/tasks/typed_adjustments_migration.rake +1 -1
- data/lib/tasks/uploads.rake +19 -0
- metadata +35 -6
- data/app/jobs/spree/products/touch_categories_job.rb +0 -15
- data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59f0dc211ad34ae5b6985f353cf1a92237d937834cc2135ad93249ee8868aab1
|
|
4
|
+
data.tar.gz: 96e00c6df2bb8f8172c231e672403cd9d25cda61a5deef1413938a327cdce4f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5000bd075a0d24d328b56f4fd5f691e9dbcd1a689f1285dcdbb4558e3c8328b3acc9a8ac44e70afff39000daa015722277e3751527c827f2146ec22c34482d4
|
|
7
|
+
data.tar.gz: 3282389785170a88dae97b1f95a51364056955ecb1d8d393e25c7f1c911d94e053681e35f1bf5e80bd4f3533fb8c0cb669e93d1d9ce898b199766970dffbeae7
|
data/README.md
CHANGED
|
@@ -40,8 +40,8 @@ Services follow a consistent interface pattern and are located in `app/services/
|
|
|
40
40
|
|
|
41
41
|
```ruby
|
|
42
42
|
# Add item to cart
|
|
43
|
-
Spree.
|
|
44
|
-
|
|
43
|
+
Spree.cart_add_item_workflow.call(
|
|
44
|
+
cart: cart,
|
|
45
45
|
variant: variant,
|
|
46
46
|
quantity: 1
|
|
47
47
|
)
|
|
@@ -76,7 +76,7 @@ Swap out default implementations with custom services:
|
|
|
76
76
|
|
|
77
77
|
```ruby
|
|
78
78
|
# config/initializers/spree.rb
|
|
79
|
-
Spree::Dependencies.
|
|
79
|
+
Spree::Dependencies.cart_add_item_workflow = 'MyCustom::CartAddItem'
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
## Configuration
|
|
@@ -10,6 +10,10 @@ module Spree
|
|
|
10
10
|
# (ActiveJob handler lookup is reverse-declaration-order).
|
|
11
11
|
discard_on ActiveJob::DeserializationError
|
|
12
12
|
|
|
13
|
+
# Bulk writers enqueue this inside their transaction; run before it
|
|
14
|
+
# commits, the job would index the product's previous prices.
|
|
15
|
+
self.enqueue_after_transaction_commit = true
|
|
16
|
+
|
|
13
17
|
# @param resource_class [String] e.g. 'Spree::Product'
|
|
14
18
|
# @param resource_id [String] always pass as string for UUID support
|
|
15
19
|
# @param store_id [String] always pass as string for UUID support
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Uploads
|
|
3
|
+
# Deletes uploaded files that were never attached to anything, once they
|
|
4
|
+
# are older than `Spree::Config.unattached_upload_retention_days`.
|
|
5
|
+
#
|
|
6
|
+
# Rails deletes nothing on its own, so an upload abandoned halfway (a
|
|
7
|
+
# closed tab, a failed form) would otherwise stay in storage forever. The
|
|
8
|
+
# retention outlasts an upload reference, which expires after a day, so no
|
|
9
|
+
# file a client can still attach is deleted.
|
|
10
|
+
#
|
|
11
|
+
# Scheduled daily by the host app (`spree/spree-starter`'s recurring.yml
|
|
12
|
+
# ships the entry). Continuable: the cursor is the last file handled, so a
|
|
13
|
+
# deploy mid-run resumes rather than starting over.
|
|
14
|
+
class PurgeUnattachedJob < Spree::BaseJob
|
|
15
|
+
include ActiveJob::Continuable
|
|
16
|
+
|
|
17
|
+
def perform
|
|
18
|
+
step :purge
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def purge(step)
|
|
24
|
+
# Only files that belong to a store: a host app may keep blobs of its
|
|
25
|
+
# own outside attachments, and those are not Spree's to delete.
|
|
26
|
+
blobs = ActiveStorage::Blob.unattached.where.not(store_id: nil).
|
|
27
|
+
where(created_at: ...Spree::Config.unattached_upload_retention_days.days.ago)
|
|
28
|
+
blobs = blobs.where(ActiveStorage::Blob.arel_table[:id].gt(step.cursor)) if step.cursor
|
|
29
|
+
|
|
30
|
+
blobs.find_each(order: :asc) do |blob|
|
|
31
|
+
purge_blob(blob)
|
|
32
|
+
step.set!(blob.id)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A file attached between the query and the purge is kept. The check
|
|
37
|
+
# covers databases that do not enforce the attachments foreign key; the
|
|
38
|
+
# rescue covers the moment between the check and the delete on those
|
|
39
|
+
# that do.
|
|
40
|
+
def purge_blob(blob)
|
|
41
|
+
return if blob.attachments.exists?
|
|
42
|
+
|
|
43
|
+
blob.purge
|
|
44
|
+
rescue ActiveRecord::InvalidForeignKey
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -72,10 +72,11 @@ module Spree
|
|
|
72
72
|
|
|
73
73
|
# Locale for an email to a staff member or seller: their own dashboard
|
|
74
74
|
# language, then the store's admin locale, then nil — which lets
|
|
75
|
-
# with_store_locale fall back to the store's default. Blank
|
|
76
|
-
#
|
|
75
|
+
# with_store_locale fall back to the store's default. Blank values, and
|
|
76
|
+
# languages Spree ships no translations for, fall through.
|
|
77
77
|
def staff_locale(user, store)
|
|
78
|
-
|
|
78
|
+
shipped = Spree.available_locales.map(&:to_s)
|
|
79
|
+
[user.try(:selected_locale), store&.preferred_admin_locale].find { |locale| shipped.include?(locale.to_s) }
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# Renders the current action's email from its Liquid template and builds
|
|
@@ -4,6 +4,7 @@ module Spree
|
|
|
4
4
|
|
|
5
5
|
include Spree::Actor
|
|
6
6
|
include Spree::PrefixedId
|
|
7
|
+
include Spree::SignedIdAttachments
|
|
7
8
|
include Spree::UserRoles
|
|
8
9
|
include Spree::RansackableAttributes
|
|
9
10
|
|
|
@@ -66,6 +67,7 @@ module Spree
|
|
|
66
67
|
|
|
67
68
|
# Attachments
|
|
68
69
|
has_one_attached :avatar, service: Spree.public_storage_service_name
|
|
70
|
+
signed_id_attachments :avatar
|
|
69
71
|
|
|
70
72
|
validates :avatar, content_type: Rails.application.config.active_storage.web_image_content_types
|
|
71
73
|
|
|
@@ -26,32 +26,60 @@ module Spree
|
|
|
26
26
|
def calculator_type=(calculator_type)
|
|
27
27
|
return if calculator_type.blank?
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
klass = registry.find { |k| k.api_type == calculator_type.to_s }
|
|
29
|
+
klass = registered_calculator_class(calculator_type)
|
|
31
30
|
self.calculator = klass.new if klass && !calculator.instance_of?(klass)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
# API v3 writer for the
|
|
35
|
-
# payload.
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
33
|
+
# API v3 writer for the `calculator: { type:, preferences: {} }`
|
|
34
|
+
# payload. A type the registry does not list is refused like a setting
|
|
35
|
+
# that does not match (`/calculator/type`); preferences are checked
|
|
36
|
+
# against the calculator's schema and written through its typed writers.
|
|
37
|
+
#
|
|
38
|
+
# @param attrs [Hash, ActionController::Parameters, nil]
|
|
39
|
+
# @param pointer [String] where the calculator sits in the request
|
|
40
|
+
# @raise [Spree::Preferences::InvalidPreferences] for a type the registry does not list, or
|
|
41
|
+
# preferences that do not match its schema
|
|
42
|
+
# @return [void]
|
|
43
|
+
def assign_calculator_attributes(attrs, pointer: '/calculator')
|
|
39
44
|
return if attrs.nil?
|
|
40
45
|
|
|
41
46
|
attrs = attrs.to_h.with_indifferent_access
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
type = attrs[:type].to_s
|
|
48
|
+
target = calculator
|
|
49
|
+
if type.present? && calculator&.class&.api_type != type
|
|
50
|
+
klass = registered_calculator_class(type)
|
|
51
|
+
unless klass
|
|
52
|
+
raise Spree::Preferences::InvalidPreferences.new(
|
|
53
|
+
[{ pointer: '/type', message: I18n.t('spree.errors.messages.unknown_calculator_type', type: type) }], prefix: pointer
|
|
54
|
+
)
|
|
55
|
+
end
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
target = klass.new
|
|
58
|
+
end
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
if attrs[:preferences].present?
|
|
61
|
+
# Preferences sent without a type land on the default calculator
|
|
62
|
+
# rather than on nothing, for a model that has one.
|
|
63
|
+
target ||= default_calculator if respond_to?(:default_calculator, true)
|
|
64
|
+
target&.assign_preferences(attrs[:preferences], pointer: "#{pointer}/preferences")
|
|
50
65
|
end
|
|
66
|
+
|
|
67
|
+
# Attached only once its settings are accepted: on a saved owner,
|
|
68
|
+
# assigning a has_one replaces the stored calculator immediately.
|
|
69
|
+
self.calculator = target unless target.equal?(calculator)
|
|
51
70
|
end
|
|
52
71
|
|
|
53
72
|
private
|
|
54
73
|
|
|
74
|
+
# The registered calculator for an API shorthand, resolved from the
|
|
75
|
+
# registry entry's own name: nothing user-supplied reaches
|
|
76
|
+
# `constantize`, and in development the registry can hold a class from
|
|
77
|
+
# an earlier reload whose instances fail the association's type check.
|
|
78
|
+
def registered_calculator_class(type)
|
|
79
|
+
registry = self.class.respond_to?(:calculators) ? self.class.calculators : []
|
|
80
|
+
registry.find { |klass| klass.api_type == type.to_s }&.to_s&.constantize
|
|
81
|
+
end
|
|
82
|
+
|
|
55
83
|
def self.model_name_without_spree_namespace
|
|
56
84
|
to_s.tableize.tr('/', '_').sub('spree_', '')
|
|
57
85
|
end
|
|
@@ -13,7 +13,7 @@ module Spree
|
|
|
13
13
|
extend ActiveSupport::Concern
|
|
14
14
|
|
|
15
15
|
included do
|
|
16
|
-
preference :amounts, :hash, default: {}
|
|
16
|
+
preference :amounts, :hash, keys: :currency, values: :money, default: {}
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# The amount this calculator quotes for a currency: the per-currency
|
|
@@ -9,24 +9,11 @@ module Spree
|
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
|
-
preference :tiers, :array,
|
|
12
|
+
preference :tiers, :array, of: :object, properties: { threshold: :money, value: :decimal }, default: []
|
|
13
13
|
|
|
14
14
|
validate :preferred_tiers_content
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# @param value [Object] tiers as an operator or API client sent them
|
|
18
|
-
# @return [Object] an array of string-keyed hashes with numbers as exact
|
|
19
|
-
# decimal strings — the form JSON stores, so a fresh value compares
|
|
20
|
-
# equal to one read back — or the value unchanged when it is not a list
|
|
21
|
-
# of hashes, so validation can name the problem
|
|
22
|
-
def self.normalize(value)
|
|
23
|
-
return value unless value.is_a?(Array) && value.all? { |tier| tier.respond_to?(:to_h) && !tier.is_a?(Array) }
|
|
24
|
-
|
|
25
|
-
value.map do |tier|
|
|
26
|
-
tier.to_h.stringify_keys.to_h { |key, number| [key, decimal(number)&.as_json || number] }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
17
|
# @return [BigDecimal, nil] nil when the value is not a number
|
|
31
18
|
def self.decimal(value)
|
|
32
19
|
BigDecimal(value.to_s, exception: false)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Decimal attributes written as a number or canonical decimal text
|
|
3
|
+
# ("1234.56"). Text in any other shape raises Spree::Money::InvalidFormat,
|
|
4
|
+
# which the API answers as `invalid_money_format`, instead of being read as
|
|
5
|
+
# a wrong number the way Rails' cast reads "1,599.99" as 1 and "$10" as 0.
|
|
6
|
+
#
|
|
7
|
+
# The columns hold four decimals, so a value with more decimals than it may
|
|
8
|
+
# carry is refused on validation rather than stored and left to drift: an
|
|
9
|
+
# amount that changes hands keeps its currency's decimals, a unit price four,
|
|
10
|
+
# a rate its column's scale.
|
|
11
|
+
module CanonicalDecimals
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# nil when the record has no currency yet (a payment before its order),
|
|
17
|
+
# which leaves the check to the record's own presence validations.
|
|
18
|
+
def canonical_decimals_currency_places
|
|
19
|
+
currency = try(:currency)
|
|
20
|
+
Spree::Money::Rounding.precision(currency) if currency.present?
|
|
21
|
+
rescue ActiveSupport::DelegationError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class_methods do
|
|
26
|
+
# @param names [Array<Symbol>]
|
|
27
|
+
# @param places [:currency, Integer, nil] decimals allowed: the record's
|
|
28
|
+
# currency's, a fixed count, or nil for no check
|
|
29
|
+
# @param ignore_blank [Boolean] leave the attribute as it was when given a blank
|
|
30
|
+
def canonical_decimals(*names, places: :currency, ignore_blank: false)
|
|
31
|
+
names.each do |name|
|
|
32
|
+
define_method("#{name}=") do |value|
|
|
33
|
+
next if ignore_blank && value.blank?
|
|
34
|
+
|
|
35
|
+
super(Spree::Money::Rounding.parse_decimal(value))
|
|
36
|
+
rescue Spree::Money::InvalidFormat => error
|
|
37
|
+
raise Spree::Money::InvalidFormat.new(error.message, field: name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return if places.nil?
|
|
42
|
+
|
|
43
|
+
validate do
|
|
44
|
+
allowed = places == :currency ? canonical_decimals_currency_places : places
|
|
45
|
+
next if allowed.nil?
|
|
46
|
+
|
|
47
|
+
names.each do |name|
|
|
48
|
+
next unless will_save_change_to_attribute?(name)
|
|
49
|
+
|
|
50
|
+
# Read before the column's scale rounds it away.
|
|
51
|
+
raw = read_attribute_before_type_cast(name)
|
|
52
|
+
value = raw.is_a?(Numeric) ? BigDecimal(raw.to_s) : self[name]
|
|
53
|
+
next if value.nil? || value.round(allowed) == value
|
|
54
|
+
|
|
55
|
+
errors.add(name, :too_many_decimals, count: allowed)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
include Spree::PrefixedId
|
|
6
|
+
include Spree::SignedIdAttachments
|
|
6
7
|
include Spree::HasCustomFields
|
|
7
8
|
include Spree::UserAddress
|
|
8
9
|
include Spree::UserPaymentSource
|
|
@@ -119,6 +120,7 @@ module Spree
|
|
|
119
120
|
# Attachments
|
|
120
121
|
#
|
|
121
122
|
has_one_attached :avatar, service: Spree.public_storage_service_name
|
|
123
|
+
signed_id_attachments :avatar
|
|
122
124
|
|
|
123
125
|
has_spree_rich_text :internal_note
|
|
124
126
|
|
|
@@ -188,9 +190,10 @@ module Spree
|
|
|
188
190
|
|
|
189
191
|
self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles orders customer_groups]
|
|
190
192
|
self.whitelisted_ransackable_attributes = %w[id email first_name last_name phone accepts_email_marketing
|
|
191
|
-
created_at updated_at
|
|
193
|
+
created_at updated_at]
|
|
192
194
|
self.whitelisted_ransackable_scopes = %w[search with_min_total_spent with_standing_for_company
|
|
193
195
|
anonymized]
|
|
196
|
+
self.ransackable_scope_types = { 'with_min_total_spent' => 'decimal', 'with_standing_for_company' => { list: 'id' }, 'anonymized' => 'boolean' }
|
|
194
197
|
|
|
195
198
|
# Two-state scope: see Spree::Base.ransack_flag? for why the cast is
|
|
196
199
|
# opted out of here and done inside the scope instead.
|
|
@@ -222,10 +225,11 @@ module Spree
|
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
# Customers with standing for a company: members of the node or of any
|
|
225
|
-
# ancestor (see #standing_for?). Accepts
|
|
226
|
-
|
|
228
|
+
# ancestor (see #standing_for?). Accepts records, ids, or an array; a
|
|
229
|
+
# splat because Ransack passes each value of a list as its own argument.
|
|
230
|
+
scope :with_standing_for_company, ->(*companies) {
|
|
227
231
|
scoped = Spree::Current.store&.companies || Spree::Company.none
|
|
228
|
-
nodes =
|
|
232
|
+
nodes = companies.flatten.filter_map do |company|
|
|
229
233
|
company.is_a?(Spree::Company) ? company : scoped.find_by_param(company)
|
|
230
234
|
end
|
|
231
235
|
next none if nodes.empty?
|
|
@@ -43,24 +43,22 @@ module Spree
|
|
|
43
43
|
# unsaved record sorts last, which is what a merchant watching a new row
|
|
44
44
|
# appear expects.
|
|
45
45
|
def position_among_siblings
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
parent = owner
|
|
47
|
+
association = sibling_association_name
|
|
48
|
+
return 1 if parent.nil? || !parent.respond_to?(association)
|
|
48
49
|
|
|
49
|
-
earlier = id.
|
|
50
|
+
earlier = if id.present? && parent.read_association_in_memory?(association)
|
|
51
|
+
parent.public_send(association).to_a.count { |sibling| sibling.id.present? && sibling.id < id }
|
|
52
|
+
else
|
|
53
|
+
scope = parent.public_send(association).unscope(:order)
|
|
54
|
+
id.nil? ? scope.count : scope.where(scope.arel_table[:id].lt(id)).count
|
|
55
|
+
end
|
|
50
56
|
|
|
51
57
|
earlier + 1
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def sibling_scope
|
|
57
|
-
parent = owner
|
|
58
|
-
return if parent.nil?
|
|
59
|
-
|
|
60
|
-
association = self.class.name.demodulize.underscore.pluralize
|
|
61
|
-
return unless parent.respond_to?(association)
|
|
62
|
-
|
|
63
|
-
parent.public_send(association).unscope(:order)
|
|
60
|
+
def sibling_association_name
|
|
61
|
+
self.class.name.demodulize.underscore.pluralize
|
|
64
62
|
end
|
|
65
63
|
end
|
|
66
64
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PreferenceSchema
|
|
3
|
+
# Compiles a class's preference declarations into the JSON Schema of its
|
|
4
|
+
# `preferences` object — the contract the OpenAPI components, the SDK
|
|
5
|
+
# types, the `/…/types` endpoints and write validation all read, so none
|
|
6
|
+
# of them is written by hand.
|
|
7
|
+
module JsonSchema
|
|
8
|
+
DECIMAL_PATTERN = '^-?\d+(\.\d+)?$'.freeze
|
|
9
|
+
|
|
10
|
+
# Domain formats, with the pattern that makes each one checkable by any
|
|
11
|
+
# validator, not only those that know the format's name.
|
|
12
|
+
FORMATS = {
|
|
13
|
+
currency: { format: 'currency', pattern: '^[A-Z]{3}$' },
|
|
14
|
+
iso_country: { format: 'iso-country', pattern: '^[A-Z]{2}$' },
|
|
15
|
+
timezone: { format: 'timezone' },
|
|
16
|
+
locale: { format: 'locale' },
|
|
17
|
+
url: { format: 'uri' },
|
|
18
|
+
color: { format: 'color', pattern: '^#[0-9A-Fa-f]{6}$' }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# @param klass [Class] a class including {Spree::PreferenceSchema}
|
|
22
|
+
# @return [Hash] a JSON Schema object (string keys)
|
|
23
|
+
def self.for(klass)
|
|
24
|
+
fields = klass.preference_schema
|
|
25
|
+
definitions = klass.preference_definitions
|
|
26
|
+
properties = fields.to_h do |field|
|
|
27
|
+
[field[:key].to_s, property_schema(definitions.fetch(field[:key]), default: field[:default])]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{ 'type' => 'object', 'properties' => properties, 'additionalProperties' => false }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The published schema plus the deprecated preferences, which a write
|
|
34
|
+
# still accepts until their removal although no read returns them.
|
|
35
|
+
#
|
|
36
|
+
# @param klass [Class]
|
|
37
|
+
# @return [Hash]
|
|
38
|
+
def self.writable(klass)
|
|
39
|
+
deprecated = klass.preference_definitions.filter_map do |name, definition|
|
|
40
|
+
[name.to_s, property_schema(definition)] if definition[:deprecated] && !definition[:internal]
|
|
41
|
+
end
|
|
42
|
+
schema = klass.preference_json_schema
|
|
43
|
+
schema.merge('properties' => schema['properties'].merge(deprecated.to_h))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The schema of one declared preference.
|
|
47
|
+
#
|
|
48
|
+
# @param definition [Hash] an entry of `preference_definitions`
|
|
49
|
+
# @param default [Object] the declared default, already evaluated
|
|
50
|
+
# @return [Hash]
|
|
51
|
+
def self.property_schema(definition, default: declared_default(definition))
|
|
52
|
+
schema = value_schema(definition)
|
|
53
|
+
choices = definition[:choices]
|
|
54
|
+
choices = choices.call if choices.respond_to?(:call)
|
|
55
|
+
schema['enum'] = choices.map { |choice| choice.is_a?(Symbol) ? choice.to_s : choice } if choices.present?
|
|
56
|
+
default = definition[:dynamic_default] ? nil : wire_default(default, definition)
|
|
57
|
+
unset_reads_null = default.nil? && !definition[:dynamic_default]
|
|
58
|
+
|
|
59
|
+
# A value nothing has set reads back as null, so a preference without
|
|
60
|
+
# a default is nullable on the wire whatever its declared type — and so
|
|
61
|
+
# is a secret, whose default is never sent.
|
|
62
|
+
if (definition[:nullable] || unset_reads_null || definition[:type] == :password) && schema['type']
|
|
63
|
+
schema['type'] = [schema['type'], 'null']
|
|
64
|
+
schema['enum'] += [nil] if schema['enum']
|
|
65
|
+
end
|
|
66
|
+
# A secret's default would leak alongside the masked value.
|
|
67
|
+
schema['default'] = default unless default.nil? || definition[:type] == :password
|
|
68
|
+
schema
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A default can be a block that reads the database (the default store's
|
|
72
|
+
# currency); one that cannot be read here is left out.
|
|
73
|
+
def self.declared_default(definition)
|
|
74
|
+
definition[:default].call
|
|
75
|
+
rescue StandardError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.value_schema(definition)
|
|
80
|
+
case definition[:type]
|
|
81
|
+
when :string then string_schema(definition[:format])
|
|
82
|
+
when :text then { 'type' => 'string', 'x-spree-widget' => 'textarea' }
|
|
83
|
+
when :boolean then { 'type' => 'boolean' }
|
|
84
|
+
when :integer then { 'type' => 'integer' }
|
|
85
|
+
when :decimal, :money then item_schema(definition[:type], definition)
|
|
86
|
+
when :password then { 'type' => 'string', 'x-spree-secret' => true }
|
|
87
|
+
when :date then { 'type' => 'string', 'format' => 'date' }
|
|
88
|
+
when :datetime then { 'type' => 'string', 'format' => 'date-time' }
|
|
89
|
+
when :array
|
|
90
|
+
schema = { 'type' => 'array' }
|
|
91
|
+
schema['items'] = item_schema(definition[:of], definition) if definition[:of]
|
|
92
|
+
schema
|
|
93
|
+
when :hash
|
|
94
|
+
schema = { 'type' => 'object' }
|
|
95
|
+
if definition[:keys] && definition[:values]
|
|
96
|
+
schema['propertyNames'] = definition[:keys] == :currency ? string_schema(:currency) : { 'type' => 'string' }
|
|
97
|
+
schema['additionalProperties'] = item_schema(definition[:values], definition)
|
|
98
|
+
end
|
|
99
|
+
schema
|
|
100
|
+
else
|
|
101
|
+
{}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.item_schema(item_type, definition)
|
|
106
|
+
case item_type
|
|
107
|
+
when :string then string_schema(definition[:format])
|
|
108
|
+
when :integer then { 'type' => 'integer' }
|
|
109
|
+
when :boolean then { 'type' => 'boolean' }
|
|
110
|
+
when :decimal then { 'type' => 'string', 'pattern' => DECIMAL_PATTERN }
|
|
111
|
+
when :money then { 'type' => 'string', 'format' => 'money', 'pattern' => DECIMAL_PATTERN }
|
|
112
|
+
when :id
|
|
113
|
+
prefix = Spree::Preferences::Preferable.preference_model(definition)._prefix_id_prefix
|
|
114
|
+
{ 'type' => 'string', 'format' => 'prefixed-id', 'pattern' => "^#{prefix}_[A-Za-z0-9]+$", 'x-spree-prefix' => prefix }
|
|
115
|
+
when :object
|
|
116
|
+
properties = definition[:properties].to_h { |name, type| [name.to_s, item_schema(type, {})] }
|
|
117
|
+
{ 'type' => 'object', 'properties' => properties, 'required' => properties.keys, 'additionalProperties' => false }
|
|
118
|
+
else
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.string_schema(format)
|
|
124
|
+
{ 'type' => 'string' }.merge(FORMATS.fetch(format, {}).transform_keys(&:to_s))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The default in the form the API takes it back: a decimal as its exact
|
|
128
|
+
# string without trailing zeros ("100", "0.23"), as every API decimal is.
|
|
129
|
+
def self.wire_default(default, definition)
|
|
130
|
+
return default if default.nil?
|
|
131
|
+
return Spree::Money::Rounding.format_decimal(default) if %i[decimal money].include?(definition[:type])
|
|
132
|
+
|
|
133
|
+
default.as_json
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_class_method :declared_default, :value_schema, :item_schema, :string_schema, :wire_default
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -50,12 +50,41 @@ module Spree
|
|
|
50
50
|
# runtime. Each entry also caches `key_string` (frozen) so hot-path
|
|
51
51
|
# serializers don't allocate `pref.to_s` per request.
|
|
52
52
|
def preference_schema
|
|
53
|
-
@preference_schema
|
|
53
|
+
return @preference_schema if @preference_schema
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
@preference_schema
|
|
55
|
+
fields, complete = compute_preference_schema
|
|
56
|
+
# Settled on only when every default could be read; one computed
|
|
57
|
+
# before the database was up is retried next time.
|
|
58
|
+
@preference_schema = fields if complete
|
|
59
|
+
fields
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The JSON Schema of this class's `preferences` object (see
|
|
63
|
+
# {Spree::PreferenceSchema::JsonSchema}). Memoized like
|
|
64
|
+
# {#preference_schema}.
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def preference_json_schema
|
|
68
|
+
return @preference_json_schema if @preference_json_schema
|
|
69
|
+
|
|
70
|
+
schema = Spree::PreferenceSchema::JsonSchema.for(self)
|
|
71
|
+
@preference_json_schema = schema if @preference_schema
|
|
72
|
+
schema
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Checks a `preferences` payload against {#preference_json_schema}, which
|
|
76
|
+
# also accepts deprecated preferences until their removal.
|
|
77
|
+
#
|
|
78
|
+
# @param values [Hash{String => Object}]
|
|
79
|
+
# @return [Array<Hash{Symbol => String}>] `{ pointer:, message: }` per failure, empty when it matches
|
|
80
|
+
def preference_failures(values)
|
|
81
|
+
# The published schema plus deprecated preferences, which a write
|
|
82
|
+
# still accepts until their removal. Memoized like the published one.
|
|
83
|
+
schemer = @preference_schemer || JSONSchemer.schema(Spree::PreferenceSchema::JsonSchema.writable(self))
|
|
84
|
+
@preference_schemer ||= schemer if @preference_schema
|
|
85
|
+
schemer.validate(values).map do |error|
|
|
86
|
+
{ pointer: error['data_pointer'], message: error['error'] }
|
|
87
|
+
end
|
|
59
88
|
end
|
|
60
89
|
|
|
61
90
|
# Wire-safe variant of `preference_schema` with `:password`
|
|
@@ -77,6 +106,10 @@ module Spree
|
|
|
77
106
|
wire = { key: field[:key], type: field[:type], default: field[:default] }
|
|
78
107
|
wire[:choices] = field[:choices] if field[:choices].present?
|
|
79
108
|
wire[:default] = nil if field[:type] == :password
|
|
109
|
+
# A decimal default goes out as the decimal string the API takes back.
|
|
110
|
+
if %i[decimal money].include?(field[:type]) && field[:default].is_a?(Numeric)
|
|
111
|
+
wire[:default] = Spree::Money::Rounding.format_decimal(field[:default])
|
|
112
|
+
end
|
|
80
113
|
wire.freeze
|
|
81
114
|
end.freeze
|
|
82
115
|
end
|
|
@@ -93,24 +126,26 @@ module Spree
|
|
|
93
126
|
.freeze
|
|
94
127
|
end
|
|
95
128
|
|
|
129
|
+
# @return [Array(Array<Hash>, Boolean)] the fields, and whether their
|
|
130
|
+
# defaults could all be read
|
|
96
131
|
def compute_preference_schema
|
|
97
132
|
# Only instantiation is guarded. `new` touches the database to read
|
|
98
133
|
# the column list, so it fails whenever the schema is asked for before
|
|
99
|
-
# a connection exists — at boot,
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
# before the database was up.
|
|
134
|
+
# a connection exists — at boot, in a rake task on an empty database,
|
|
135
|
+
# or while generating the SDK types. An uninitialized instance still
|
|
136
|
+
# describes every preference, since the declarations are pure Ruby;
|
|
137
|
+
# only a default read from the database is missing, so that result is
|
|
138
|
+
# not memoized. An error describing the preferences themselves is a
|
|
139
|
+
# real bug and is left to raise.
|
|
140
|
+
complete = true
|
|
107
141
|
instance = begin
|
|
108
142
|
new
|
|
109
143
|
rescue StandardError
|
|
110
|
-
|
|
144
|
+
complete = false
|
|
145
|
+
allocate
|
|
111
146
|
end
|
|
112
147
|
|
|
113
|
-
instance.defined_preferences.filter_map do |pref|
|
|
148
|
+
fields = instance.defined_preferences.filter_map do |pref|
|
|
114
149
|
next if instance.preference_deprecated(pref)
|
|
115
150
|
# Written by Spree, not supplied by the operator — a value a
|
|
116
151
|
# provider hands back after we register something with it. Offering
|
|
@@ -126,6 +161,7 @@ module Spree
|
|
|
126
161
|
choices: instance.preference_choices(pref)
|
|
127
162
|
}.compact.freeze
|
|
128
163
|
end
|
|
164
|
+
[fields, complete]
|
|
129
165
|
end
|
|
130
166
|
|
|
131
167
|
# Builds a `parse_on_set:` lambda for `preference :foo_ids, :array`
|
|
@@ -150,6 +186,9 @@ module Spree
|
|
|
150
186
|
# relation builder; defaults to the unscoped `klass`.
|
|
151
187
|
# @return [Proc] suitable for the `parse_on_set:` preference option.
|
|
152
188
|
def normalize_id_preference(klass: nil, scope: nil)
|
|
189
|
+
Spree::Deprecation.warn(
|
|
190
|
+
'`normalize_id_preference` is deprecated. Declare the list as `preference :name, :array, of: :id, model:, scope:`.'
|
|
191
|
+
)
|
|
153
192
|
lambda do |values, owner = nil|
|
|
154
193
|
raw = Array(values).flat_map { |v| v.to_s.split(',') }.compact_blank.map(&:strip)
|
|
155
194
|
next raw unless klass
|
|
@@ -178,11 +217,10 @@ module Spree
|
|
|
178
217
|
registered_subclasses.find { |klass| klass.api_type == shorthand.to_s }
|
|
179
218
|
end
|
|
180
219
|
|
|
181
|
-
# Returns a `[{ type:, label:, description:,
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
# discovery surface and must never leak gateway-shipped defaults.
|
|
220
|
+
# Returns a `[{ type:, label:, description:, schema: }]` array for every
|
|
221
|
+
# registered subclass, sorted by label for stable output. `schema` is
|
|
222
|
+
# the JSON Schema of the subclass's `preferences`, which never carries a
|
|
223
|
+
# secret's default.
|
|
186
224
|
#
|
|
187
225
|
# `label` and `description` are a fallback, not the admin UI's copy:
|
|
188
226
|
# they resolve in the request's locale, which is the store's rather
|
|
@@ -199,7 +237,7 @@ module Spree
|
|
|
199
237
|
# Asking for only one left promotion types with a null description
|
|
200
238
|
# and no fallback for a client without its own translation.
|
|
201
239
|
description: subclass_description(klass),
|
|
202
|
-
|
|
240
|
+
schema: klass.preference_json_schema
|
|
203
241
|
}
|
|
204
242
|
# Only present when true, so families without the concept keep
|
|
205
243
|
# their wire shape byte-identical. Pickers stop offering a
|