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
|
@@ -1,39 +1,61 @@
|
|
|
1
|
-
# A rule to limit a promotion based on shipment country.
|
|
1
|
+
# A rule to limit a promotion based on shipment country. Stores an
|
|
2
|
+
# array of ISO codes — countries are inherently identified by ISO
|
|
3
|
+
# in the API. The legacy single `country_id` / `country_iso`
|
|
4
|
+
# preferences still work; they fold into the multi-country list.
|
|
2
5
|
module Spree
|
|
3
6
|
class Promotion
|
|
4
7
|
module Rules
|
|
5
8
|
class Country < PromotionRule
|
|
6
|
-
preference :
|
|
7
|
-
|
|
9
|
+
preference :country_isos, :array, default: [], parse_on_set: lambda { |values|
|
|
10
|
+
normalize_id_preference.call(values).map(&:upcase)
|
|
11
|
+
}
|
|
12
|
+
preference :country_id, :integer # legacy single-country shortcut
|
|
13
|
+
preference :country_iso, :string # legacy ISO-based shortcut
|
|
8
14
|
|
|
9
15
|
def applicable?(promotable)
|
|
10
16
|
promotable.is_a?(Spree::Order)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
19
|
+
def countries
|
|
20
|
+
isos = preferred_country_isos.presence || [preferred_country_iso].compact_blank
|
|
21
|
+
return Spree::Country.none if isos.blank?
|
|
22
|
+
|
|
23
|
+
Spree::Country.where(iso: isos.map { |s| s.to_s.upcase })
|
|
19
24
|
end
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
def eligible?(order, options = {})
|
|
27
|
+
allowed_isos = eligible_country_isos(order)
|
|
28
|
+
shipping_iso = options[:country_iso] || order.ship_address&.country_iso
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
country_id = options[:country_id] || order.ship_address.try(:country_id)
|
|
25
|
-
return true if country_id == (preferred_country_id || order.store.default_country_id)
|
|
30
|
+
return true if allowed_isos.include?(shipping_iso)
|
|
26
31
|
|
|
27
32
|
eligibility_errors.add(:base, eligibility_error_message(:wrong_country))
|
|
28
33
|
false
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
# Effective list of eligible country ISOs, merging legacy
|
|
37
|
+
# single-country preferences into the multi-country list.
|
|
38
|
+
# Order-of-precedence: explicit ISO list > legacy single ISO
|
|
39
|
+
# > legacy single ID > store default. Memoized per-instance —
|
|
40
|
+
# eligibility checks fire repeatedly per cart change.
|
|
41
|
+
def eligible_country_isos(order = nil)
|
|
42
|
+
@eligible_country_isos ||= compute_eligible_country_isos(order)
|
|
43
|
+
end
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def compute_eligible_country_isos(order)
|
|
48
|
+
return preferred_country_isos.map { |v| v.to_s.upcase } if preferred_country_isos.present?
|
|
49
|
+
return [preferred_country_iso.to_s.upcase] if preferred_country_iso.present?
|
|
50
|
+
|
|
51
|
+
if preferred_country_id.present?
|
|
52
|
+
iso = Spree::Country.where(id: preferred_country_id).pick(:iso)
|
|
53
|
+
return [iso.to_s.upcase] if iso.present?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return [] if order.nil?
|
|
57
|
+
|
|
58
|
+
[order.store&.default_country&.iso, order.store&.default_market&.default_country&.iso].compact.map(&:upcase).uniq
|
|
37
59
|
end
|
|
38
60
|
end
|
|
39
61
|
end
|
|
@@ -2,12 +2,21 @@ module Spree
|
|
|
2
2
|
class Promotion
|
|
3
3
|
module Rules
|
|
4
4
|
class CustomerGroup < PromotionRule
|
|
5
|
-
|
|
5
|
+
# Stored as raw IDs. Accepts prefixed IDs (`cg_…`) from API
|
|
6
|
+
# callers and decodes them on write so eligibility checks can
|
|
7
|
+
# compare against raw `customer_group_id` rows directly.
|
|
8
|
+
preference :customer_group_ids, :array, default: [], parse_on_set: normalize_id_preference(klass: Spree::CustomerGroup)
|
|
6
9
|
|
|
7
10
|
def applicable?(promotable)
|
|
8
11
|
promotable.is_a?(Spree::Order)
|
|
9
12
|
end
|
|
10
13
|
|
|
14
|
+
def customer_groups
|
|
15
|
+
return Spree::CustomerGroup.none if preferred_customer_group_ids.blank?
|
|
16
|
+
|
|
17
|
+
Spree::CustomerGroup.where(id: preferred_customer_group_ids)
|
|
18
|
+
end
|
|
19
|
+
|
|
11
20
|
def eligible?(order, _options = {})
|
|
12
21
|
return false unless order.user_id.present?
|
|
13
22
|
return false if preferred_customer_group_ids.empty?
|
|
@@ -10,11 +10,34 @@ module Spree
|
|
|
10
10
|
dependent: :destroy
|
|
11
11
|
has_many :taxons, through: :promotion_rule_taxons, class_name: 'Spree::Taxon'
|
|
12
12
|
|
|
13
|
+
def self.additional_permitted_attributes
|
|
14
|
+
[category_ids: []]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Wire-format shorthand is `category` (the model is still `Taxon`
|
|
18
|
+
# pre-6.0 rename). `key` (instance) cascades through `api_type`.
|
|
19
|
+
def self.api_type
|
|
20
|
+
'category'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# PrefixedId's auto-resolver in `assign_attributes` only fires
|
|
24
|
+
# when the `_ids` stem matches an association — `categories`
|
|
25
|
+
# doesn't, so decode prefixed IDs explicitly here.
|
|
26
|
+
def category_ids=(ids)
|
|
27
|
+
self.taxon_ids = Array(ids).map do |id|
|
|
28
|
+
Spree::PrefixedId.prefixed_id?(id) ? Spree::Taxon.find_by_param!(id).id : id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def category_ids
|
|
33
|
+
taxon_ids
|
|
34
|
+
end
|
|
35
|
+
|
|
13
36
|
#
|
|
14
37
|
# Preferences
|
|
15
38
|
#
|
|
16
39
|
MATCH_POLICIES = %w(any all)
|
|
17
|
-
preference :match_policy, default: MATCH_POLICIES.first
|
|
40
|
+
preference :match_policy, :string, default: MATCH_POLICIES.first
|
|
18
41
|
|
|
19
42
|
#
|
|
20
43
|
# Attributes
|
|
@@ -10,6 +10,27 @@ module Spree
|
|
|
10
10
|
dependent: :destroy
|
|
11
11
|
has_many :users, through: :promotion_rule_users, class_name: "::#{Spree.user_class}"
|
|
12
12
|
|
|
13
|
+
# Customers, not admin users — the rule keys off `Spree::Order#user_id`.
|
|
14
|
+
# The data layer keeps the `users` association (legacy column name);
|
|
15
|
+
# the API exposes the same set as `customer_ids`.
|
|
16
|
+
def self.additional_permitted_attributes
|
|
17
|
+
[customer_ids: []]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Wire-format shorthand is `customer` (the model is still `User`
|
|
21
|
+
# pre-6.0 rename, see docs/plans/6.0-platform-auth.md).
|
|
22
|
+
def self.api_type
|
|
23
|
+
'customer'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def customer_ids
|
|
27
|
+
user_ids
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def customer_ids=(ids)
|
|
31
|
+
self.user_ids = ids
|
|
32
|
+
end
|
|
33
|
+
|
|
13
34
|
#
|
|
14
35
|
# Attributes
|
|
15
36
|
#
|
|
@@ -2,6 +2,12 @@ module Spree
|
|
|
2
2
|
class Promotion
|
|
3
3
|
module Rules
|
|
4
4
|
class UserLoggedIn < PromotionRule
|
|
5
|
+
# Wire-format shorthand is `customer_logged_in` (the model is still
|
|
6
|
+
# `UserLoggedIn` pre-6.0 rename, see docs/plans/6.0-platform-auth.md).
|
|
7
|
+
def self.api_type
|
|
8
|
+
'customer_logged_in'
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
def applicable?(promotable)
|
|
6
12
|
promotable.is_a?(Spree::Order)
|
|
7
13
|
end
|
|
@@ -39,7 +39,8 @@ module Spree
|
|
|
39
39
|
has_many :orders, through: :order_promotions, class_name: 'Spree::Order'
|
|
40
40
|
has_many :store_promotions, class_name: 'Spree::StorePromotion'
|
|
41
41
|
has_many :stores, class_name: 'Spree::Store', through: :store_promotions
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
after_save :apply_pending_rules_and_actions, if: :pending_rules_or_actions?
|
|
43
44
|
|
|
44
45
|
#
|
|
45
46
|
# Callbacks
|
|
@@ -115,6 +116,21 @@ module Spree
|
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
|
|
119
|
+
# Flat-payload writer for `rules`. See
|
|
120
|
+
# {Spree::TypedAssociations#assign_typed_association}.
|
|
121
|
+
def rules=(rows)
|
|
122
|
+
assign_typed_association(:promotion_rules, rows)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Mirrors `rules=` for promotion actions.
|
|
126
|
+
def actions=(rows)
|
|
127
|
+
assign_typed_association(:promotion_actions, rows)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def pending_rules_or_actions?
|
|
131
|
+
@pending_promotion_rules.present? || @pending_promotion_actions.present?
|
|
132
|
+
end
|
|
133
|
+
|
|
118
134
|
def active?
|
|
119
135
|
starts_at.present? && starts_at < Time.current && (expires_at.blank? || !expired?)
|
|
120
136
|
end
|
|
@@ -283,6 +299,11 @@ module Spree
|
|
|
283
299
|
|
|
284
300
|
private
|
|
285
301
|
|
|
302
|
+
def apply_pending_rules_and_actions
|
|
303
|
+
flush_pending_typed_association(:promotion_rules)
|
|
304
|
+
flush_pending_typed_association(:promotion_actions)
|
|
305
|
+
end
|
|
306
|
+
|
|
286
307
|
def not_used?
|
|
287
308
|
return true if orders.empty?
|
|
288
309
|
|
|
@@ -12,6 +12,15 @@ module Spree
|
|
|
12
12
|
|
|
13
13
|
scope :of_type, ->(t) { where(type: t) }
|
|
14
14
|
|
|
15
|
+
# Per-subclass permitted attributes beyond `type` and `preferences`.
|
|
16
|
+
# Override in STI subclasses that accept nested attributes (e.g.
|
|
17
|
+
# CreateLineItems needs `promotion_action_line_items_attributes`,
|
|
18
|
+
# CreateAdjustment needs `calculator_type` + `calculator_attributes`).
|
|
19
|
+
# The Admin API merges these into its `params.permit(...)` allowlist.
|
|
20
|
+
def self.additional_permitted_attributes
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
15
24
|
# This method should be overridden in subclass
|
|
16
25
|
# Updates the state of the order or performs some other action depending on the subclass
|
|
17
26
|
# options will contain the payload from the event that activated the promotion. This will include
|
|
@@ -27,25 +36,22 @@ module Spree
|
|
|
27
36
|
type == 'Spree::Promotion::Actions::FreeShipping'
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# @return [String] eg. Free Shipping
|
|
33
|
-
def human_name
|
|
34
|
-
Spree.t("promotion_action_types.#{key}.name")
|
|
39
|
+
def self.human_name
|
|
40
|
+
Spree.t("promotion_action_types.#{api_type}.name", default: api_type.titleize)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# @return [String]
|
|
40
|
-
def human_description
|
|
41
|
-
Spree.t("promotion_action_types.#{key}.description")
|
|
43
|
+
def self.human_description
|
|
44
|
+
Spree.t("promotion_action_types.#{api_type}.description", default: '')
|
|
42
45
|
end
|
|
43
46
|
|
|
47
|
+
def human_name = self.class.human_name
|
|
48
|
+
def human_description = self.class.human_description
|
|
49
|
+
|
|
44
50
|
# Returns the key of the promotion action
|
|
45
51
|
#
|
|
46
52
|
# @return [String] eg. free_shipping
|
|
47
53
|
def key
|
|
48
|
-
|
|
54
|
+
self.class.api_type
|
|
49
55
|
end
|
|
50
56
|
|
|
51
57
|
protected
|
|
@@ -10,7 +10,15 @@ module Spree
|
|
|
10
10
|
scope :of_type, ->(t) { where(type: t) }
|
|
11
11
|
|
|
12
12
|
validates :promotion, presence: true
|
|
13
|
-
|
|
13
|
+
validates :type, uniqueness: { scope: [:promotion_id, *spree_base_uniqueness_scope] }
|
|
14
|
+
|
|
15
|
+
# Per-subclass permitted attributes beyond `type` and `preferences`.
|
|
16
|
+
# Override in STI subclasses that accept association IDs (e.g.
|
|
17
|
+
# Rules::Product needs `product_ids`). The Admin API merges these
|
|
18
|
+
# into its `params.permit(...)` allowlist.
|
|
19
|
+
def self.additional_permitted_attributes
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
14
22
|
|
|
15
23
|
def self.for(promotable)
|
|
16
24
|
all.select { |rule| rule.applicable?(promotable) }
|
|
@@ -34,35 +42,26 @@ module Spree
|
|
|
34
42
|
@eligibility_errors ||= ActiveModel::Errors.new(self)
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# @return [String] eg. Currency
|
|
40
|
-
def human_name
|
|
41
|
-
Spree.t("promotion_rule_types.#{key}.name")
|
|
45
|
+
def self.human_name
|
|
46
|
+
Spree.t("promotion_rule_types.#{api_type}.name", default: api_type.titleize)
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# @return [String]
|
|
47
|
-
def human_description
|
|
48
|
-
Spree.t("promotion_rule_types.#{key}.description")
|
|
49
|
+
def self.human_description
|
|
50
|
+
Spree.t("promotion_rule_types.#{api_type}.description", default: '')
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
def human_name = self.class.human_name
|
|
54
|
+
def human_description = self.class.human_description
|
|
55
|
+
|
|
51
56
|
# Returns the key of the promotion rule
|
|
52
57
|
#
|
|
53
58
|
# @return [String] eg. currency
|
|
54
59
|
def key
|
|
55
|
-
|
|
60
|
+
self.class.api_type
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
private
|
|
59
64
|
|
|
60
|
-
def unique_per_promotion
|
|
61
|
-
if Spree::PromotionRule.exists?(promotion_id: promotion_id, type: self.class.name)
|
|
62
|
-
errors.add(:base, 'Promotion already contains this rule type')
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
65
|
def eligibility_error_message(key, options = {})
|
|
67
66
|
Spree.t(key, Hash[scope: [:eligibility_errors, :messages]].merge(options))
|
|
68
67
|
end
|
|
@@ -233,7 +233,7 @@ module Spree
|
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
def filterable_attributes
|
|
236
|
-
%w[product_id status in_stock store_ids locale currency discontinue_on price category_ids tags option_value_ids]
|
|
236
|
+
%w[product_id status in_stock store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids]
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
def sortable_attributes
|
|
@@ -259,12 +259,22 @@ module Spree
|
|
|
259
259
|
# System scoping — always applied. Rarely overridden.
|
|
260
260
|
# Mirrors the AR scope: store.products.active(currency) with locale.
|
|
261
261
|
def system_filter_conditions
|
|
262
|
+
now = Time.current
|
|
262
263
|
conditions = []
|
|
263
264
|
conditions << "store_ids = '#{store.id}'"
|
|
265
|
+
conditions << "channel_ids = '#{Spree::Current.channel.id}'" if Spree::Current.channel
|
|
264
266
|
conditions << "status = 'active'"
|
|
265
267
|
conditions << "locale = '#{locale.to_s.gsub(/[^a-zA-Z_-]/, '')}'"
|
|
266
268
|
conditions << "currency = '#{currency.to_s.gsub(/[^A-Z]/, '')}'"
|
|
267
|
-
|
|
269
|
+
# Exclude future-dated products — mirrors +Product.available(Time.current)+.
|
|
270
|
+
# ISO 8601 strings sort lexicographically in chronological order, so the
|
|
271
|
+
# string compare is sound. +NOT EXISTS+ catches docs indexed before this
|
|
272
|
+
# attribute was emitted (legacy indexes), +IS NULL+ catches docs where
|
|
273
|
+
# the field was emitted as explicit null, and the +<=+ clause filters
|
|
274
|
+
# the remaining future-dated docs — so the upgrade is non-breaking and
|
|
275
|
+
# no reindex is required.
|
|
276
|
+
conditions << "(available_on NOT EXISTS OR available_on IS NULL OR available_on <= '#{now.iso8601}')"
|
|
277
|
+
conditions << "(discontinue_on = 0 OR discontinue_on > #{now.to_i})"
|
|
268
278
|
conditions
|
|
269
279
|
end
|
|
270
280
|
|
|
@@ -28,7 +28,7 @@ module Spree
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
30
|
def item_available?(line_item, quantity)
|
|
31
|
-
Spree::Stock::Quantifier.new(line_item.variant).can_supply?(quantity)
|
|
31
|
+
Spree::Stock::Quantifier.new(line_item.variant, excluded_order: line_item.order).can_supply?(quantity)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -1,25 +1,89 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Stock
|
|
3
3
|
class Quantifier
|
|
4
|
-
attr_reader :variant, :stock_location
|
|
4
|
+
attr_reader :variant, :stock_location, :excluded_order
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# @param excluded_order [Spree::Order, nil] when given, reservations
|
|
7
|
+
# belonging to this order are not counted against availability. Used
|
|
8
|
+
# when checking an order's own line items so the customer's own
|
|
9
|
+
# checkout hold doesn't make their item look out of stock.
|
|
10
|
+
def initialize(variant, stock_location = nil, excluded_order: nil)
|
|
11
|
+
@variant = variant
|
|
12
|
+
@stock_location = stock_location
|
|
13
|
+
@excluded_order = excluded_order
|
|
9
14
|
end
|
|
10
15
|
|
|
16
|
+
# Units a customer can purchase right now: physical pool minus
|
|
17
|
+
# already-allocated units minus active checkout reservations. Clamped
|
|
18
|
+
# at zero so callers never see a negative count.
|
|
19
|
+
#
|
|
20
|
+
# Returns +BigDecimal::INFINITY+ when the variant does not track
|
|
21
|
+
# inventory (effectively unlimited supply).
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer, BigDecimal] purchasable quantity, or +INFINITY+
|
|
11
24
|
def total_on_hand
|
|
12
25
|
@total_on_hand ||= if variant.should_track_inventory?
|
|
13
|
-
|
|
14
|
-
stock_items.sum(&:count_on_hand)
|
|
15
|
-
else
|
|
16
|
-
stock_items.sum(:count_on_hand)
|
|
17
|
-
end
|
|
26
|
+
[available_stock - reserved_quantity, 0].max
|
|
18
27
|
else
|
|
19
28
|
BigDecimal::INFINITY
|
|
20
29
|
end
|
|
21
30
|
end
|
|
22
31
|
|
|
32
|
+
# Physical pool minus already-allocated units, summed across the
|
|
33
|
+
# variant's active stock items.
|
|
34
|
+
#
|
|
35
|
+
# In Spree 5.5 {Spree::StockItem#allocated_count} is a Ruby shim that
|
|
36
|
+
# always returns 0, so this equals +SUM(count_on_hand)+. In 6.0
|
|
37
|
+
# (Typed Stock Movements) +allocated_count+ becomes a real column and
|
|
38
|
+
# the SQL path subtracts it natively.
|
|
39
|
+
#
|
|
40
|
+
# @return [Integer] units available before checkout reservations
|
|
41
|
+
def available_stock
|
|
42
|
+
if association_loaded?
|
|
43
|
+
stock_items.sum(&:available_count)
|
|
44
|
+
elsif self.class.allocated_count_column?
|
|
45
|
+
stock_items.sum('count_on_hand - allocated_count')
|
|
46
|
+
else
|
|
47
|
+
stock_items.sum(:count_on_hand)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Units currently held by active checkout reservations on the
|
|
52
|
+
# location-filtered stock items. Returns 0 when stock reservations
|
|
53
|
+
# are globally disabled.
|
|
54
|
+
#
|
|
55
|
+
# Reads through the same {#stock_items} collection as {#available_stock}
|
|
56
|
+
# so a per-location query (filtered by `stock_location`) only counts
|
|
57
|
+
# reservations that belong to those same stock items — otherwise a
|
|
58
|
+
# multi-location variant would subtract reservations from other
|
|
59
|
+
# warehouses.
|
|
60
|
+
#
|
|
61
|
+
# When +excluded_order+ is set, that order's own reservations are left
|
|
62
|
+
# out of the count so an order's own checkout hold doesn't count
|
|
63
|
+
# against the availability of its own line items.
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer]
|
|
66
|
+
def reserved_quantity
|
|
67
|
+
return @reserved_quantity if defined?(@reserved_quantity)
|
|
68
|
+
return @reserved_quantity = 0 unless Spree::Config[:stock_reservations_enabled]
|
|
69
|
+
return @reserved_quantity = 0 if stock_items.blank?
|
|
70
|
+
|
|
71
|
+
excluded_order_id = excluded_order&.id
|
|
72
|
+
|
|
73
|
+
@reserved_quantity = if reservations_preloaded?
|
|
74
|
+
stock_items.sum do |si|
|
|
75
|
+
reservations = si.active_stock_reservations
|
|
76
|
+
reservations = reservations.reject { |r| r.order_id == excluded_order_id } if excluded_order_id
|
|
77
|
+
reservations.sum(&:quantity)
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
reservations = Spree::StockReservation.active.where(stock_item_id: stock_items.map(&:id))
|
|
81
|
+
reservations = reservations.where.not(order_id: excluded_order_id) if excluded_order_id
|
|
82
|
+
reservations.sum(:quantity)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if any of variant stock items is backorderable
|
|
23
87
|
def backorderable?
|
|
24
88
|
@backorderable ||= stock_items.any?(&:backorderable)
|
|
25
89
|
end
|
|
@@ -32,12 +96,28 @@ module Spree
|
|
|
32
96
|
@stock_items ||= scope_to_location(variant.stock_items)
|
|
33
97
|
end
|
|
34
98
|
|
|
99
|
+
# Memoized schema check so {#available_stock} doesn't introspect the
|
|
100
|
+
# column list on every call. Flips from false → true when 6.0 Typed
|
|
101
|
+
# Stock Movements adds the `allocated_count` column.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def self.allocated_count_column?
|
|
105
|
+
return @allocated_count_column if defined?(@allocated_count_column)
|
|
106
|
+
|
|
107
|
+
@allocated_count_column = Spree::StockItem.connection.column_exists?(:spree_stock_items, :allocated_count)
|
|
108
|
+
end
|
|
109
|
+
|
|
35
110
|
private
|
|
36
111
|
|
|
37
112
|
def association_loaded?
|
|
38
113
|
variant.association(:stock_items).loaded?
|
|
39
114
|
end
|
|
40
115
|
|
|
116
|
+
def reservations_preloaded?
|
|
117
|
+
association_loaded? &&
|
|
118
|
+
stock_items.all? { |si| si.association(:active_stock_reservations).loaded? }
|
|
119
|
+
end
|
|
120
|
+
|
|
41
121
|
def scope_to_location(collection)
|
|
42
122
|
if stock_location.blank?
|
|
43
123
|
if association_loaded?
|
|
@@ -15,6 +15,8 @@ module Spree
|
|
|
15
15
|
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
|
|
16
16
|
end
|
|
17
17
|
has_many :stock_movements, inverse_of: :stock_item
|
|
18
|
+
has_many :stock_reservations, class_name: 'Spree::StockReservation', inverse_of: :stock_item, dependent: :destroy
|
|
19
|
+
has_many :active_stock_reservations, -> { active }, class_name: 'Spree::StockReservation', inverse_of: :stock_item
|
|
18
20
|
|
|
19
21
|
validates :stock_location, :variant, presence: true
|
|
20
22
|
validates :variant_id, uniqueness: { scope: :stock_location_id }, unless: :deleted_at
|
|
@@ -38,6 +40,18 @@ module Spree
|
|
|
38
40
|
|
|
39
41
|
scope :with_active_stock_location, -> { joins(:stock_location).merge(Spree::StockLocation.active) }
|
|
40
42
|
|
|
43
|
+
# Stock items for products assigned to `store`. Walks
|
|
44
|
+
# `variant → product → stores`, dedups via `distinct` so a product
|
|
45
|
+
# in multiple stores doesn't double-count its stock items.
|
|
46
|
+
#
|
|
47
|
+
# Used by the admin API as the base scope (`StockItem.for_store`)
|
|
48
|
+
# so the controller can filter directly by stock_location/variant
|
|
49
|
+
# without inheriting `Spree::Store#stock_items`'s extra joins or
|
|
50
|
+
# the variant default ordering.
|
|
51
|
+
scope :for_store, ->(store) {
|
|
52
|
+
joins(variant: :product).where(spree_products: { store_id: store.id })
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
def backordered_inventory_units
|
|
42
56
|
Spree::InventoryUnit.backordered_for_stock_item(self)
|
|
43
57
|
end
|
|
@@ -64,6 +78,28 @@ module Spree
|
|
|
64
78
|
in_stock? || backorderable?
|
|
65
79
|
end
|
|
66
80
|
|
|
81
|
+
# Units already allocated to pending shipments at this stock item.
|
|
82
|
+
#
|
|
83
|
+
# Always returns 0 in Spree 5.5. The 6.0 Typed Stock Movements plan
|
|
84
|
+
# (see docs/plans/6.0-typed-stock-movements.md) adds an indexed
|
|
85
|
+
# `allocated_count` column updated by typed movements (`allocated`,
|
|
86
|
+
# `released`, `shipped`); the Rails column accessor then takes
|
|
87
|
+
# precedence over this method automatically.
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
def allocated_count
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Physical stock minus allocated units at this stock item. Distinct from
|
|
95
|
+
# {Spree::Stock::Quantifier#available_stock}, which sums this across all
|
|
96
|
+
# stock items belonging to a variant.
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer]
|
|
99
|
+
def available_count
|
|
100
|
+
count_on_hand - allocated_count
|
|
101
|
+
end
|
|
102
|
+
|
|
67
103
|
def reduce_count_on_hand_to_zero
|
|
68
104
|
set_count_on_hand(0) if count_on_hand > 0
|
|
69
105
|
end
|
|
@@ -2,6 +2,15 @@ module Spree
|
|
|
2
2
|
class StockLocation < Spree.base_class
|
|
3
3
|
has_prefix_id :sloc # Spree-specific: stock location
|
|
4
4
|
|
|
5
|
+
# Categorizes the location. Open string — extensible by setting any value;
|
|
6
|
+
# KINDS lists the built-in options used by the admin UI dropdown.
|
|
7
|
+
KINDS = %w[warehouse store fulfillment_center].freeze
|
|
8
|
+
|
|
9
|
+
# Pickup stock policy: 'local' = only items physically at this location are
|
|
10
|
+
# collectable; 'any' = items can be transferred from other locations
|
|
11
|
+
# (ship-to-store). See docs/plans/6.0-fulfillment-and-delivery.md.
|
|
12
|
+
PICKUP_STOCK_POLICIES = %w[local any].freeze
|
|
13
|
+
|
|
5
14
|
include Spree::UniqueName
|
|
6
15
|
if defined?(Spree::Security::StockLocations)
|
|
7
16
|
include Spree::Security::StockLocations
|
|
@@ -20,14 +29,41 @@ module Spree
|
|
|
20
29
|
belongs_to :state, class_name: 'Spree::State', optional: true
|
|
21
30
|
belongs_to :country, class_name: 'Spree::Country'
|
|
22
31
|
|
|
32
|
+
validates :kind, presence: true
|
|
33
|
+
validates :pickup_stock_policy, inclusion: { in: PICKUP_STOCK_POLICIES }
|
|
34
|
+
validates :pickup_ready_in_minutes,
|
|
35
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 },
|
|
36
|
+
allow_nil: true
|
|
37
|
+
|
|
38
|
+
self.whitelisted_ransackable_attributes = %w[
|
|
39
|
+
name active default kind pickup_enabled
|
|
40
|
+
country_id state_id created_at updated_at
|
|
41
|
+
]
|
|
42
|
+
|
|
23
43
|
scope :active, -> { where(active: true) }
|
|
44
|
+
scope :pickup_enabled, -> { where(pickup_enabled: true) }
|
|
24
45
|
scope :order_default, -> { order(default: :desc, name: :asc) }
|
|
25
46
|
|
|
47
|
+
before_validation :normalize_country
|
|
48
|
+
before_validation :normalize_state
|
|
49
|
+
|
|
26
50
|
after_create :create_stock_items, if: :propagate_all_variants?
|
|
27
51
|
after_save :ensure_one_default
|
|
28
52
|
after_update :conditional_touch_records
|
|
29
53
|
|
|
30
54
|
delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true, allow_nil: true
|
|
55
|
+
delegate :abbr, to: :state, prefix: true, allow_nil: true
|
|
56
|
+
|
|
57
|
+
# Writer methods for API convenience — accept ISO/abbr codes instead of FK IDs.
|
|
58
|
+
# Mirrors Spree::Address: SDK clients use country_iso/state_abbr because
|
|
59
|
+
# Country/State don't expose prefixed IDs (their `iso` is the public handle).
|
|
60
|
+
def country_iso=(value)
|
|
61
|
+
@country_iso_input = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def state_abbr=(value)
|
|
65
|
+
@state_abbr_input = value
|
|
66
|
+
end
|
|
31
67
|
|
|
32
68
|
def state_text
|
|
33
69
|
state.try(:abbr) || state.try(:name) || state_name
|
|
@@ -168,6 +204,22 @@ module Spree
|
|
|
168
204
|
|
|
169
205
|
private
|
|
170
206
|
|
|
207
|
+
def normalize_country
|
|
208
|
+
iso = @country_iso_input
|
|
209
|
+
return if iso.blank?
|
|
210
|
+
|
|
211
|
+
self.country = Spree::Country.by_iso(iso)
|
|
212
|
+
@country_iso_input = nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def normalize_state
|
|
216
|
+
abbr = @state_abbr_input
|
|
217
|
+
return if abbr.blank? || country.blank?
|
|
218
|
+
|
|
219
|
+
self.state = country.states.find_by(abbr: abbr)
|
|
220
|
+
@state_abbr_input = nil
|
|
221
|
+
end
|
|
222
|
+
|
|
171
223
|
def create_stock_items
|
|
172
224
|
Spree::StockLocations::StockItems::CreateJob.perform_later(self)
|
|
173
225
|
end
|