spree_core 6.0.0.rc1 → 6.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -18,9 +18,16 @@ require 'spree/core/preferences/preferable_class_methods'
|
|
|
18
18
|
module Spree::Preferences::Preferable
|
|
19
19
|
extend ActiveSupport::Concern
|
|
20
20
|
|
|
21
|
+
BOOLEAN_TYPE = ActiveModel::Type::Boolean.new
|
|
22
|
+
INTEGER_TYPE = ActiveModel::Type::Integer.new
|
|
23
|
+
STRING_TYPE = ActiveModel::Type::String.new
|
|
24
|
+
|
|
21
25
|
included do
|
|
22
26
|
serialize :preferences, coder: Spree::Metadata::HashSerializer
|
|
23
27
|
extend Spree::Preferences::PreferableClassMethods
|
|
28
|
+
|
|
29
|
+
# The preferences read and written under their plain names (see `exposes_preferences`).
|
|
30
|
+
class_attribute :exposed_preference_names, instance_accessor: false, default: Set.new.freeze
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
# JSON keeps decimals and times as strings; the declared type turns them
|
|
@@ -34,7 +41,7 @@ module Spree::Preferences::Preferable
|
|
|
34
41
|
return value unless value.is_a?(String)
|
|
35
42
|
|
|
36
43
|
case type
|
|
37
|
-
when :decimal then BigDecimal(value, exception: false) || value
|
|
44
|
+
when :decimal, :money then BigDecimal(value, exception: false) || value
|
|
38
45
|
when :datetime then Time.zone.iso8601(value)
|
|
39
46
|
else value
|
|
40
47
|
end
|
|
@@ -42,6 +49,17 @@ module Spree::Preferences::Preferable
|
|
|
42
49
|
value
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
# The record class an `of: :id` preference points at. Declared as a class
|
|
53
|
+
# name or a block, so a declaration never loads another model, nor fixes a
|
|
54
|
+
# configurable class like `Spree.customer_class`, at class load.
|
|
55
|
+
#
|
|
56
|
+
# @param definition [Hash] a preference definition
|
|
57
|
+
# @return [Class]
|
|
58
|
+
def self.preference_model(definition)
|
|
59
|
+
model = definition[:model]
|
|
60
|
+
model.respond_to?(:call) ? model.call : model.to_s.constantize
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
def get_preference(name)
|
|
46
64
|
has_preference! name
|
|
47
65
|
public_send(:"preferred_#{name}")
|
|
@@ -81,6 +99,24 @@ module Spree::Preferences::Preferable
|
|
|
81
99
|
choices.respond_to?(:call) ? choices.call : choices
|
|
82
100
|
end
|
|
83
101
|
|
|
102
|
+
# The currency a `:money` preference is in: its declared `currency:` source
|
|
103
|
+
# (another preference, or a method of the record), else the record's own
|
|
104
|
+
# `currency` preference. Values about to be assigned win over stored ones,
|
|
105
|
+
# so a request changing both the amount and its currency is read as one.
|
|
106
|
+
#
|
|
107
|
+
# @param name [Symbol, String]
|
|
108
|
+
# @param pending [Hash{String => Object}] values not yet assigned
|
|
109
|
+
# @return [String, nil] nil for an amount in any currency
|
|
110
|
+
def money_preference_currency(name, pending = {})
|
|
111
|
+
declared = preference_definition(name)[:currency]
|
|
112
|
+
source = declared || :currency
|
|
113
|
+
# A currency sent in the same payload wins, even a blank one clearing it.
|
|
114
|
+
return pending[source.to_s].presence if pending.key?(source.to_s)
|
|
115
|
+
return get_preference(source).presence if has_preference?(source)
|
|
116
|
+
|
|
117
|
+
public_send(source).presence if declared && respond_to?(source)
|
|
118
|
+
end
|
|
119
|
+
|
|
84
120
|
def has_preference!(name)
|
|
85
121
|
raise NoMethodError, "#{name} preference not defined" unless has_preference? name
|
|
86
122
|
end
|
|
@@ -142,6 +178,53 @@ module Spree::Preferences::Preferable
|
|
|
142
178
|
clear_attribute_change(:preferences) if persisted? && !already_changed
|
|
143
179
|
end
|
|
144
180
|
|
|
181
|
+
# Writes a `preferences` payload an API client sent. The whole payload is
|
|
182
|
+
# checked against the type's schema first, so nothing is written when any
|
|
183
|
+
# value is wrong. Then each value goes through its typed writer. A secret
|
|
184
|
+
# sent back masked, as it was read, keeps the stored one; `null` clears it.
|
|
185
|
+
#
|
|
186
|
+
# @param values [Hash, ActionController::Parameters]
|
|
187
|
+
# @param pointer [String] where the preferences object sits in the request,
|
|
188
|
+
# e.g. `/rules/1/preferences`, so a failure names its place
|
|
189
|
+
# @raise [Spree::Preferences::InvalidPreferences] naming every value that does not match
|
|
190
|
+
# @return [void]
|
|
191
|
+
def assign_preferences(values, pointer: '/preferences')
|
|
192
|
+
values = values.respond_to?(:to_unsafe_h) ? values.to_unsafe_h : values.to_h
|
|
193
|
+
values = values.deep_stringify_keys
|
|
194
|
+
failures = self.class.preference_failures(values)
|
|
195
|
+
failures += money_precision_failures(values) if failures.empty?
|
|
196
|
+
raise Spree::Preferences::InvalidPreferences.new(failures, prefix: pointer) if failures.any?
|
|
197
|
+
|
|
198
|
+
values.each do |key, value|
|
|
199
|
+
next if secret_preference?(key) && Spree::Preferences::Masking.masked?(value)
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
set_preference(key, value)
|
|
203
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
204
|
+
raise Spree::Preferences::InvalidPreferences.new([{ pointer: "/#{key}", message: e.message }], prefix: pointer)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# A stored value in the form the schema describes. Rows written before the
|
|
210
|
+
# declarations were typed can hold a number where an exact decimal string
|
|
211
|
+
# is declared (an `amounts` hash, a decimal default from YAML), or a string
|
|
212
|
+
# where a number or boolean is; read back as they are, a client sending
|
|
213
|
+
# them unchanged would be refused.
|
|
214
|
+
#
|
|
215
|
+
# @param value [Object] the stored value
|
|
216
|
+
# @param definition [Hash] its preference definition
|
|
217
|
+
# @return [Object]
|
|
218
|
+
def wire_preference_value(value, definition)
|
|
219
|
+
return BigDecimal(value.to_s).as_json if %i[decimal money].include?(definition[:type]) && value.is_a?(Numeric)
|
|
220
|
+
return nil if value == '' && definition[:nullable] && %i[integer boolean].include?(definition[:type])
|
|
221
|
+
return cast_preference_item(value, :integer) if definition[:type] == :integer && value.is_a?(String)
|
|
222
|
+
return convert_preference_value(value, :boolean, nullable: definition[:nullable]) if definition[:type] == :boolean && value.is_a?(String)
|
|
223
|
+
return value if value.nil? || (definition[:type] == :array && !value.is_a?(Array))
|
|
224
|
+
|
|
225
|
+
cast_preference_contents(value, definition)
|
|
226
|
+
end
|
|
227
|
+
|
|
145
228
|
# Names of the preferences the last save changed, secrets included.
|
|
146
229
|
#
|
|
147
230
|
# @return [Array<Symbol>]
|
|
@@ -151,6 +234,24 @@ module Spree::Preferences::Preferable
|
|
|
151
234
|
|
|
152
235
|
private
|
|
153
236
|
|
|
237
|
+
# A `:money` value with more decimals than its currency has is refused, as
|
|
238
|
+
# every other amount on the API is. The schema has already checked the
|
|
239
|
+
# format; it cannot know the currency.
|
|
240
|
+
def money_precision_failures(values)
|
|
241
|
+
values.filter_map do |key, value|
|
|
242
|
+
next unless value.is_a?(String) && has_preference?(key) && preference_type(key) == :money
|
|
243
|
+
|
|
244
|
+
currency = money_preference_currency(key, values)
|
|
245
|
+
amount = BigDecimal(value, exception: false)
|
|
246
|
+
next if currency.blank? || amount.nil?
|
|
247
|
+
|
|
248
|
+
places = Spree::Money::Rounding.precision(currency)
|
|
249
|
+
next if amount.round(places) == amount
|
|
250
|
+
|
|
251
|
+
{ pointer: "/#{key}", message: I18n.t('spree.errors.messages.money_preference_too_many_decimals', count: places, currency: currency) }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
154
255
|
def preference_definition(name)
|
|
155
256
|
has_preference! name
|
|
156
257
|
self.class.preference_definitions[name.to_sym]
|
|
@@ -162,7 +263,7 @@ module Spree::Preferences::Preferable
|
|
|
162
263
|
def stored_default(name)
|
|
163
264
|
default = preference_default(name)
|
|
164
265
|
type = preference_type(name)
|
|
165
|
-
return default if default.nil? || %i[decimal datetime].exclude?(type)
|
|
266
|
+
return default if default.nil? || %i[decimal money datetime].exclude?(type)
|
|
166
267
|
|
|
167
268
|
convert_preference_value(default, type, nullable: true).as_json
|
|
168
269
|
end
|
|
@@ -182,16 +283,95 @@ module Spree::Preferences::Preferable
|
|
|
182
283
|
# `(value, owner = nil)` shapes.
|
|
183
284
|
value = parse_on_set.arity.abs > 1 ? parse_on_set.call(value, self) : parse_on_set.call(value)
|
|
184
285
|
end
|
|
286
|
+
# Spree 6.0 bridge, removed with incomplete declarations in 6.1: an id list
|
|
287
|
+
# an extension declared without `of: :id` still has its prefixed ids
|
|
288
|
+
# decoded, as the API did for every `*_ids` key before.
|
|
289
|
+
if definition[:type] == :array && definition[:of].nil? && name.to_s.end_with?('_ids') && value.is_a?(Array)
|
|
290
|
+
value = value.map { |id| (Spree::PrefixedId.decode_prefixed_id(id) if Spree::PrefixedId.prefixed_id?(id)) || id }
|
|
291
|
+
end
|
|
185
292
|
value = convert_preference_value(value, definition[:type], nullable: definition[:nullable])
|
|
293
|
+
value = cast_preference_contents(value, definition)
|
|
186
294
|
# Decimals and times are kept in the string form JSON stores, so a record
|
|
187
295
|
# read back from the database and one just written compare equal and
|
|
188
296
|
# setting the same value is not a change. The reader restores them.
|
|
189
|
-
value = value.as_json if %i[decimal datetime].include?(definition[:type])
|
|
297
|
+
value = value.as_json if %i[decimal money datetime].include?(definition[:type])
|
|
190
298
|
|
|
191
299
|
Spree::Deprecation.warn("`#{name}` is deprecated. #{definition[:deprecated]}") if definition[:deprecated]
|
|
192
300
|
value
|
|
193
301
|
end
|
|
194
302
|
|
|
303
|
+
# Casts what a typed `:array` or `:hash` holds to its declared item type, in
|
|
304
|
+
# the form JSON stores. A value that does not cast is left as it came, so
|
|
305
|
+
# validation can name it rather than a silent zero hiding it.
|
|
306
|
+
def cast_preference_contents(value, definition)
|
|
307
|
+
case definition[:type]
|
|
308
|
+
when :array
|
|
309
|
+
case definition[:of]
|
|
310
|
+
when nil then value
|
|
311
|
+
when :id then decode_preference_ids(value, definition)
|
|
312
|
+
when :object then value.map { |item| cast_preference_object(item, definition[:properties]) }
|
|
313
|
+
else split_preference_list(value).map { |item| cast_preference_item(item, definition[:of]) }
|
|
314
|
+
end
|
|
315
|
+
when :hash
|
|
316
|
+
return value unless definition[:values] && value.is_a?(Hash)
|
|
317
|
+
|
|
318
|
+
value.to_h do |key, item|
|
|
319
|
+
key = definition[:keys] == :currency ? key.to_s.upcase : key.to_s
|
|
320
|
+
[key, cast_preference_item(item, definition[:values])]
|
|
321
|
+
end
|
|
322
|
+
else
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# A comma-separated string is a list too: what a plain text field sends.
|
|
328
|
+
def split_preference_list(values)
|
|
329
|
+
values.flat_map { |item| item.is_a?(String) ? item.split(',') : [item] }
|
|
330
|
+
.map { |item| item.is_a?(String) ? item.strip : item }
|
|
331
|
+
.reject { |item| item.respond_to?(:empty?) && item.empty? }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def cast_preference_item(value, item_type)
|
|
335
|
+
case item_type
|
|
336
|
+
when :string then value.to_s
|
|
337
|
+
when :integer then Integer(value.to_s, 10, exception: false) || value
|
|
338
|
+
when :decimal, :money then BigDecimal(value.to_s, exception: false)&.as_json || value
|
|
339
|
+
when :boolean then BOOLEAN_TYPE.cast(value)
|
|
340
|
+
else value
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def cast_preference_object(item, properties)
|
|
345
|
+
return item unless item.respond_to?(:to_h) && !item.is_a?(Array)
|
|
346
|
+
|
|
347
|
+
item.to_h.stringify_keys.slice(*properties.keys.map(&:to_s)).to_h do |key, value|
|
|
348
|
+
[key, cast_preference_item(value, properties[key.to_sym])]
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Turns a list of record ids into the raw primary keys the preference
|
|
353
|
+
# stores. A prefixed id must carry the declared model's prefix — a
|
|
354
|
+
# channel's id never decodes into a market id — and every id must belong to
|
|
355
|
+
# the record's scope, so a rule cannot point at another store's records.
|
|
356
|
+
#
|
|
357
|
+
# @raise [ActiveRecord::RecordNotFound] naming the ids that were not found
|
|
358
|
+
def decode_preference_ids(values, definition)
|
|
359
|
+
model = Spree::Preferences::Preferable.preference_model(definition)
|
|
360
|
+
ids = split_preference_list(values).map(&:to_s).map do |id|
|
|
361
|
+
Spree::PrefixedId.prefixed_id?(id) ? (model.decode_prefixed_id(id)&.to_s || id) : id
|
|
362
|
+
end
|
|
363
|
+
return ids if ids.empty?
|
|
364
|
+
|
|
365
|
+
relation = definition[:scope] ? definition[:scope].call(self) : model
|
|
366
|
+
found = relation.where(id: ids.reject { |id| Spree::PrefixedId.prefixed_id?(id) }).pluck(:id).map(&:to_s).to_set
|
|
367
|
+
missing = ids.reject { |id| found.include?(id) }
|
|
368
|
+
if missing.any?
|
|
369
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{model.name} with id=#{missing.join(',')}", model.name)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
ids
|
|
373
|
+
end
|
|
374
|
+
|
|
195
375
|
# Assigns a new hash rather than calling the store accessor's writer, which
|
|
196
376
|
# skips writing nil to a missing key — leaving the default in force when nil
|
|
197
377
|
# was meant. Rails still sees no change when the value is the one stored.
|
|
@@ -206,40 +386,46 @@ module Spree::Preferences::Preferable
|
|
|
206
386
|
end
|
|
207
387
|
end
|
|
208
388
|
|
|
389
|
+
# Whitespace alone is no number or boolean, so for those it is blank too; a
|
|
390
|
+
# string keeps it.
|
|
391
|
+
def blank_preference_value?(value, type = nil)
|
|
392
|
+
return true if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
393
|
+
|
|
394
|
+
%i[integer boolean].include?(type) && value.is_a?(String) && value.strip.empty?
|
|
395
|
+
end
|
|
396
|
+
|
|
209
397
|
def convert_preference_value(value, type, nullable: false)
|
|
210
398
|
case type
|
|
399
|
+
# Strings, integers and booleans cast the way Rails casts attributes (and
|
|
400
|
+
# `has_json` its keys). Our own rule on top: a blank value stays nil for a
|
|
401
|
+
# nullable preference, so it can fall back to another, and becomes the
|
|
402
|
+
# type's empty value otherwise.
|
|
211
403
|
when :string, :text
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
215
|
-
nil
|
|
404
|
+
if blank_preference_value?(value)
|
|
405
|
+
nullable ? nil : ''
|
|
216
406
|
else
|
|
217
|
-
value
|
|
407
|
+
STRING_TYPE.cast(value)
|
|
218
408
|
end
|
|
219
409
|
when :password
|
|
220
410
|
value.to_s
|
|
221
|
-
when :decimal
|
|
222
|
-
|
|
411
|
+
when :decimal, :money
|
|
412
|
+
# A number or canonical decimal text only: "1,599.99" raises rather
|
|
413
|
+
# than being read as 1.
|
|
414
|
+
decimal_value = value.is_a?(String) ? value.presence : value
|
|
223
415
|
decimal_value ||= 0 unless nullable
|
|
224
|
-
decimal_value.
|
|
416
|
+
decimal_value.nil? ? nil : Spree::Money::Rounding.parse_decimal(decimal_value)
|
|
225
417
|
when :integer
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
418
|
+
if blank_preference_value?(value, :integer)
|
|
419
|
+
nullable ? nil : 0
|
|
420
|
+
else
|
|
421
|
+
INTEGER_TYPE.cast(value) || 0
|
|
422
|
+
end
|
|
229
423
|
when :boolean
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
234
|
-
nil
|
|
235
|
-
elsif value.is_a?(FalseClass) ||
|
|
236
|
-
value.nil? ||
|
|
237
|
-
value == 0 ||
|
|
238
|
-
value&.to_s =~ /^(f|false|0)$/i ||
|
|
239
|
-
(value.respond_to?(:empty?) && value.empty?)
|
|
240
|
-
false
|
|
424
|
+
# An explicit false is kept: it is neither nil nor empty.
|
|
425
|
+
if blank_preference_value?(value, :boolean)
|
|
426
|
+
nullable ? nil : false
|
|
241
427
|
else
|
|
242
|
-
|
|
428
|
+
BOOLEAN_TYPE.cast(value)
|
|
243
429
|
end
|
|
244
430
|
when :array
|
|
245
431
|
value.is_a?(Array) ? value : Array.wrap(value)
|
|
@@ -37,12 +37,42 @@ module Spree::Preferences
|
|
|
37
37
|
preference_declarations_cache[:secrets] ||= declared_preference_types.filter_map { |name, type| name if type == :password }.freeze
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# The value types a list item, a hash value or an object property may
|
|
41
|
+
# declare — what {Spree::PreferenceSchema::JsonSchema} compiles to JSON
|
|
42
|
+
# Schema, along with its `FORMATS`.
|
|
43
|
+
ITEM_TYPES = %i[string integer decimal money boolean id object].freeze
|
|
44
|
+
|
|
40
45
|
# Declares a typed preference with a `preferred_<name>` reader and writer
|
|
41
46
|
# and a `prefers_<name>?` query. On a model, the value lives in the
|
|
42
47
|
# `preferences` column (`secret_preferences` for a `:password` one) as a
|
|
43
48
|
# Rails store accessor, which brings the change methods with it:
|
|
44
49
|
# `preferred_<name>_changed?`, `_change`, `_was`,
|
|
45
50
|
# `saved_change_to_preferred_<name>?` and `preferred_<name>_before_last_save`.
|
|
51
|
+
#
|
|
52
|
+
# The declaration is the preference's contract on the wire, so it states
|
|
53
|
+
# the full type:
|
|
54
|
+
#
|
|
55
|
+
# preference :amount_min, :money
|
|
56
|
+
# preference :channel_ids, :array, of: :id, model: 'Spree::Channel', scope: ->(rule) { rule.store.channels }
|
|
57
|
+
# preference :country_codes, :array, of: :string, format: :iso_country
|
|
58
|
+
# preference :match_policy, :string, choices: %w[any all none]
|
|
59
|
+
# preference :amounts, :hash, keys: :currency, values: :money
|
|
60
|
+
# preference :tiers, :array, of: :object, properties: { threshold: :money, value: :decimal }
|
|
61
|
+
#
|
|
62
|
+
# @param name [Symbol]
|
|
63
|
+
# @param type [Symbol] `:string`, `:text`, `:boolean`, `:integer`, `:decimal`, `:money`,
|
|
64
|
+
# `:array`, `:hash`, `:password`, `:date` or `:datetime`
|
|
65
|
+
# @option options [Symbol] :of the type of each item of an `:array`
|
|
66
|
+
# @option options [Symbol] :keys the type of a `:hash`'s keys (`:string` or `:currency`)
|
|
67
|
+
# @option options [Symbol] :values the type of a `:hash`'s values
|
|
68
|
+
# @option options [Hash{Symbol => Symbol}] :properties the fields of each `of: :object` item
|
|
69
|
+
# @option options [Symbol] :format a string's domain format (see {Spree::PreferenceSchema::JsonSchema::FORMATS})
|
|
70
|
+
# @option options [String, Proc] :model the record class an `of: :id` list points at
|
|
71
|
+
# @option options [Proc] :scope `->(owner) { relation }` the ids must be found in
|
|
72
|
+
# @option options [Array, Proc] :choices the fixed set a value must come from
|
|
73
|
+
# @option options [Symbol] :currency where a `:money` preference's currency comes
|
|
74
|
+
# from: another preference or a method of the record. Defaults to the
|
|
75
|
+
# record's `currency` preference; with neither, the amount is in any currency.
|
|
46
76
|
def preference(name, type, *args)
|
|
47
77
|
name = name.to_sym
|
|
48
78
|
secret = type == :password
|
|
@@ -52,20 +82,39 @@ module Spree::Preferences
|
|
|
52
82
|
end
|
|
53
83
|
|
|
54
84
|
options = args.extract_options!
|
|
55
|
-
options.assert_valid_keys(:default, :deprecated, :in, :internal, :nullable, :parse_on_set
|
|
85
|
+
options.assert_valid_keys(:default, :deprecated, :in, :choices, :internal, :nullable, :parse_on_set,
|
|
86
|
+
:of, :keys, :values, :properties, :format, :model, :scope, :currency)
|
|
87
|
+
if options.key?(:in)
|
|
88
|
+
Spree::Deprecation.warn("`preference :#{name}, in:` is deprecated. Use `choices:` instead.")
|
|
89
|
+
options[:choices] ||= options.delete(:in)
|
|
90
|
+
end
|
|
91
|
+
check_preference_declaration(name, type, options)
|
|
92
|
+
|
|
56
93
|
default = options[:default]
|
|
57
|
-
|
|
94
|
+
dynamic_default = default.is_a?(Proc)
|
|
95
|
+
default = -> { options[:default] } unless dynamic_default
|
|
58
96
|
own_preference_definitions[name] = {
|
|
59
97
|
type: type,
|
|
60
98
|
default: default,
|
|
99
|
+
# Computed when read (the default store's currency, say), so it
|
|
100
|
+
# differs between stores and is not part of the preference's schema.
|
|
101
|
+
dynamic_default: dynamic_default,
|
|
61
102
|
deprecated: options[:deprecated],
|
|
62
103
|
# Whether the system writes the value rather than the operator — a
|
|
63
104
|
# value a provider hands back, kept out of every admin form.
|
|
64
105
|
internal: options[:internal],
|
|
65
106
|
# The fixed set a value must come from; turns a text box into a picker.
|
|
66
|
-
choices: options[:
|
|
107
|
+
choices: options[:choices],
|
|
67
108
|
nullable: options[:nullable],
|
|
68
|
-
parse_on_set: options[:parse_on_set]
|
|
109
|
+
parse_on_set: options[:parse_on_set],
|
|
110
|
+
of: options[:of],
|
|
111
|
+
keys: options[:keys],
|
|
112
|
+
values: options[:values],
|
|
113
|
+
properties: options[:properties],
|
|
114
|
+
format: options[:format],
|
|
115
|
+
model: options[:model],
|
|
116
|
+
scope: options[:scope],
|
|
117
|
+
currency: options[:currency]
|
|
69
118
|
}
|
|
70
119
|
PreferableClassMethods.declarations += 1
|
|
71
120
|
|
|
@@ -86,8 +135,76 @@ module Spree::Preferences
|
|
|
86
135
|
end
|
|
87
136
|
end
|
|
88
137
|
|
|
138
|
+
# Gives each named preference a plain reader, writer and predicate, so the
|
|
139
|
+
# API reads and writes `guest_checkout` rather than the DSL's
|
|
140
|
+
# `preferred_guest_checkout` — the methods Rails' `has_delegated_json`
|
|
141
|
+
# defines, so callers would not change if the storage ever moved to it.
|
|
142
|
+
# Grants no write access: each API controller still lists what it permits.
|
|
143
|
+
#
|
|
144
|
+
# exposes_preferences :guest_checkout, :timezone
|
|
145
|
+
#
|
|
146
|
+
# @param names [Array<Symbol>]
|
|
147
|
+
# @return [void]
|
|
148
|
+
def exposes_preferences(*names)
|
|
149
|
+
names.each do |name|
|
|
150
|
+
name = name.to_sym
|
|
151
|
+
raise ArgumentError, "#{self.name} has no preference `#{name}` to expose" unless preference_definitions.key?(name)
|
|
152
|
+
raise ArgumentError, "#{self.name} cannot expose the preference `#{name}`: a method of that name exists" if method_defined?(name)
|
|
153
|
+
|
|
154
|
+
define_method(name) { public_send(:"preferred_#{name}") }
|
|
155
|
+
define_method(:"#{name}=") { |value| public_send(:"preferred_#{name}=", value) }
|
|
156
|
+
# As `has_json` defines one; a predicate the model already has (one
|
|
157
|
+
# that falls back to another setting, say) is kept.
|
|
158
|
+
define_method(:"#{name}?") { public_send(:"preferred_#{name}").present? } unless method_defined?(:"#{name}?")
|
|
159
|
+
end
|
|
160
|
+
self.exposed_preference_names = (exposed_preference_names | names.map(&:to_sym)).freeze
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The plain name an exposed preference goes by, for a key still using the
|
|
164
|
+
# DSL's `preferred_` prefix — an error key or a 5.x request parameter.
|
|
165
|
+
#
|
|
166
|
+
# @param key [Symbol, String] e.g. `preferred_guest_checkout`
|
|
167
|
+
# @return [Symbol, nil] e.g. `:guest_checkout`, or nil when the key names no exposed preference
|
|
168
|
+
def exposed_preference_name(key)
|
|
169
|
+
name = key.to_s.delete_prefix('preferred_')
|
|
170
|
+
name.to_sym if name != key.to_s && exposed_preference_names.include?(name.to_sym)
|
|
171
|
+
end
|
|
172
|
+
|
|
89
173
|
private
|
|
90
174
|
|
|
175
|
+
# A declaration is the preference's contract on the wire. One Spree ships
|
|
176
|
+
# is always complete (a spec enforces it); an incomplete one from an
|
|
177
|
+
# extension still works in 6.0, with a schema that accepts any value.
|
|
178
|
+
def check_preference_declaration(name, type, options)
|
|
179
|
+
if options[:of] && type != :array
|
|
180
|
+
raise ArgumentError, "`of:` applies to an :array preference, not `#{name}` (#{type})"
|
|
181
|
+
end
|
|
182
|
+
if (options[:keys] || options[:values]) && type != :hash
|
|
183
|
+
raise ArgumentError, "`keys:` and `values:` apply to a :hash preference, not `#{name}` (#{type})"
|
|
184
|
+
end
|
|
185
|
+
[options[:of], options[:values], *options[:properties]&.values].compact.each do |item_type|
|
|
186
|
+
raise ArgumentError, "Unknown item type `#{item_type}` on preference `#{name}`" unless ITEM_TYPES.include?(item_type)
|
|
187
|
+
end
|
|
188
|
+
if options[:format] && !Spree::PreferenceSchema::JsonSchema::FORMATS.key?(options[:format])
|
|
189
|
+
raise ArgumentError, "Unknown format `#{options[:format]}` on preference `#{name}`"
|
|
190
|
+
end
|
|
191
|
+
raise ArgumentError, "`of: :id` needs `model:` on preference `#{name}`" if options[:of] == :id && options[:model].blank?
|
|
192
|
+
raise ArgumentError, "`currency:` applies to a :money preference, not `#{name}` (#{type})" if options[:currency] && type != :money
|
|
193
|
+
raise ArgumentError, "`of: :object` needs `properties:` on preference `#{name}`" if options[:of] == :object && options[:properties].blank?
|
|
194
|
+
|
|
195
|
+
missing = case type
|
|
196
|
+
when :any then 'a type other than :any'
|
|
197
|
+
when :array then '`of:`' unless options[:of]
|
|
198
|
+
when :hash then '`keys:` and `values:`' unless options[:keys] && options[:values]
|
|
199
|
+
end
|
|
200
|
+
return unless missing
|
|
201
|
+
|
|
202
|
+
Spree::Deprecation.warn(
|
|
203
|
+
"#{self.name} preference `#{name}` (#{type}) needs #{missing}; until it has one, its schema accepts any value. " \
|
|
204
|
+
'Spree 6.1 will raise for an incomplete declaration.'
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
91
208
|
def own_preference_definitions
|
|
92
209
|
@own_preference_definitions ||= {}
|
|
93
210
|
end
|
|
@@ -6,7 +6,7 @@ module Spree
|
|
|
6
6
|
#
|
|
7
7
|
# @example Adding custom searchable fields
|
|
8
8
|
# Spree.ransack.add_attribute(Spree::Product, :seller_id)
|
|
9
|
-
# Spree.ransack.add_scope(Spree::Product, :by_seller)
|
|
9
|
+
# Spree.ransack.add_scope(Spree::Product, :by_seller, type: 'id')
|
|
10
10
|
# Spree.ransack.add_association(Spree::Product, :seller)
|
|
11
11
|
#
|
|
12
12
|
class RansackConfiguration
|
|
@@ -14,6 +14,7 @@ module Spree
|
|
|
14
14
|
@custom_attributes = Hash.new { |h, k| h[k] = [] }
|
|
15
15
|
@custom_associations = Hash.new { |h, k| h[k] = [] }
|
|
16
16
|
@custom_scopes = Hash.new { |h, k| h[k] = [] }
|
|
17
|
+
@custom_scope_types = Hash.new { |h, k| h[k] = {} }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# Add a custom ransackable attribute to a model.
|
|
@@ -38,8 +39,11 @@ module Spree
|
|
|
38
39
|
#
|
|
39
40
|
# @param model [Class] the model class to configure (e.g., Spree::Product)
|
|
40
41
|
# @param scope [String, Symbol] the scope to add
|
|
42
|
+
# @param type [String, Array<String>, Hash, nil] the arguments it takes, as
|
|
43
|
+
# in `ransackable_scope_types` — `'boolean'` for a scope taking none
|
|
41
44
|
# @return [Array<String>] the updated list of custom scopes
|
|
42
|
-
def add_scope(model, scope)
|
|
45
|
+
def add_scope(model, scope, type: nil)
|
|
46
|
+
@custom_scope_types[model.name.to_sym][scope.to_s] = type unless type.nil?
|
|
43
47
|
@custom_scopes[model.name.to_sym] |= [scope.to_s]
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -67,6 +71,12 @@ module Spree
|
|
|
67
71
|
@custom_scopes[model.name.to_sym]
|
|
68
72
|
end
|
|
69
73
|
|
|
74
|
+
# @param model [Class] the model class to query
|
|
75
|
+
# @return [Hash{String => Object}] argument types declared with {#add_scope}
|
|
76
|
+
def custom_scope_types_for(model)
|
|
77
|
+
@custom_scope_types[model.name.to_sym]
|
|
78
|
+
end
|
|
79
|
+
|
|
70
80
|
# Reset all custom configurations. Useful for testing.
|
|
71
81
|
#
|
|
72
82
|
# @return [void]
|
|
@@ -74,6 +84,7 @@ module Spree
|
|
|
74
84
|
@custom_attributes.clear
|
|
75
85
|
@custom_associations.clear
|
|
76
86
|
@custom_scopes.clear
|
|
87
|
+
@custom_scope_types.clear
|
|
77
88
|
end
|
|
78
89
|
end
|
|
79
90
|
end
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/core.rb
CHANGED
|
@@ -17,6 +17,7 @@ require 'awesome_nested_set'
|
|
|
17
17
|
require 'cancan'
|
|
18
18
|
require 'countries/global'
|
|
19
19
|
require 'friendly_id'
|
|
20
|
+
require 'json_schemer'
|
|
20
21
|
require 'jwt'
|
|
21
22
|
require 'liquid'
|
|
22
23
|
require 'spree/core/emails/escaped_output'
|
|
@@ -251,7 +252,7 @@ module Spree
|
|
|
251
252
|
# Example:
|
|
252
253
|
#
|
|
253
254
|
# Spree.dependencies do |dependency|
|
|
254
|
-
# dependency.
|
|
255
|
+
# dependency.cart_add_item_workflow = MyCustomAddToCart
|
|
255
256
|
# end
|
|
256
257
|
#
|
|
257
258
|
# This method is defined within the core gem on purpose.
|
|
@@ -542,10 +543,10 @@ module Spree
|
|
|
542
543
|
# Dynamic methods for core dependencies
|
|
543
544
|
#
|
|
544
545
|
# @example Getting a dependency (returns resolved class)
|
|
545
|
-
# Spree.
|
|
546
|
+
# Spree.cart_add_item_workflow.call(cart: cart, variant: variant)
|
|
546
547
|
#
|
|
547
548
|
# @example Setting a dependency
|
|
548
|
-
# Spree.
|
|
549
|
+
# Spree.cart_add_item_workflow = MyApp::CartAddItem
|
|
549
550
|
def method_missing(method_name, *args, &block)
|
|
550
551
|
base_name = method_name.to_s.chomp('=').to_sym
|
|
551
552
|
|
|
@@ -635,6 +636,7 @@ require 'spree/workflow'
|
|
|
635
636
|
require 'spree/reporting'
|
|
636
637
|
require 'spree/events'
|
|
637
638
|
require 'spree/store_scope_guard'
|
|
639
|
+
require 'spree/uploads'
|
|
638
640
|
|
|
639
641
|
# Not autoloaded from app/: the registry keeps registered steps in
|
|
640
642
|
# class-level state, which a reload would discard.
|
|
@@ -649,6 +651,7 @@ require 'spree/core/controller_helpers/store'
|
|
|
649
651
|
|
|
650
652
|
require 'spree/core/preferences/runtime_configuration'
|
|
651
653
|
require 'spree/core/preferences/masking'
|
|
654
|
+
require 'spree/core/preferences/invalid_preferences'
|
|
652
655
|
require 'spree/core/preferences/json_conversion'
|
|
653
656
|
|
|
654
657
|
require 'spree/core/permission_configuration'
|
data/lib/spree/iso_data.rb
CHANGED
|
@@ -88,6 +88,13 @@ module Spree
|
|
|
88
88
|
freeze
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# The alpha-2 code of every orderable country, the closed list the API
|
|
92
|
+
# types publish as `CountryCode`.
|
|
93
|
+
# @return [Array<String>]
|
|
94
|
+
def country_codes
|
|
95
|
+
@country_codes ||= countries.map(&:alpha2).sort.freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
91
98
|
# @param code [String] alpha-2 or alpha-3 code, any case
|
|
92
99
|
# @return [ISO3166::Country, nil]
|
|
93
100
|
def country(code)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Deprecated: Spree no longer parses numbers by locale. Its APIs take
|
|
3
|
+
# canonical decimal strings ("1234.56"); localizing input is the client's job.
|
|
2
4
|
class LocalizedNumber
|
|
3
5
|
# Strips all non-price-like characters from the number, taking into account locale settings.
|
|
4
6
|
def self.parse(number)
|
|
7
|
+
Spree::Deprecation.warn(
|
|
8
|
+
'Spree::LocalizedNumber is deprecated and will be removed in Spree 6.1. ' \
|
|
9
|
+
'Parse a canonical decimal string with BigDecimal, or Spree::Money::Rounding.parse_canonical for API input.'
|
|
10
|
+
)
|
|
5
11
|
return number unless number.is_a?(String)
|
|
6
12
|
|
|
7
13
|
separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter'])
|
data/lib/spree/migrations.rb
CHANGED
|
@@ -29,8 +29,11 @@ module Spree
|
|
|
29
29
|
|
|
30
30
|
name
|
|
31
31
|
end.compact
|
|
32
|
+
installed = engine_in_app.map(&:camelize).to_set
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
# Compared by class name: installing a migration writes its file name
|
|
35
|
+
# back from the class, so `decimal_19_4` is installed as `decimal194`.
|
|
36
|
+
missing_migrations = engine_migrations.sort.reject { |name| installed.include?(name.camelize) }
|
|
34
37
|
unless missing_migrations.empty?
|
|
35
38
|
puts "[#{engine_name.capitalize} WARNING] Missing migrations."
|
|
36
39
|
missing_migrations.each do |migration|
|