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
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :sub
|
|
4
4
|
|
|
5
5
|
include Spree::Metafields
|
|
6
|
+
include Spree::SingleStoreResource
|
|
6
7
|
|
|
7
8
|
publishes_lifecycle_events
|
|
8
9
|
|
|
@@ -12,6 +13,7 @@ module Spree
|
|
|
12
13
|
# Associations
|
|
13
14
|
#
|
|
14
15
|
belongs_to :user, optional: true, class_name: Spree.user_class&.name
|
|
16
|
+
belongs_to :store, class_name: 'Spree::Store', required: true
|
|
15
17
|
|
|
16
18
|
#
|
|
17
19
|
# Validations
|
|
@@ -19,7 +21,7 @@ module Spree
|
|
|
19
21
|
validates :email,
|
|
20
22
|
presence: true,
|
|
21
23
|
format: { with: URI::MailTo::EMAIL_REGEXP },
|
|
22
|
-
uniqueness: { case_sensitive: false, scope: spree_base_uniqueness_scope }
|
|
24
|
+
uniqueness: { case_sensitive: false, scope: spree_base_uniqueness_scope + [:store_id] }
|
|
23
25
|
|
|
24
26
|
#
|
|
25
27
|
# Scopes
|
|
@@ -30,6 +32,7 @@ module Spree
|
|
|
30
32
|
#
|
|
31
33
|
# Callbacks
|
|
32
34
|
#
|
|
35
|
+
before_validation :set_store, unless: :store_id?
|
|
33
36
|
normalizes :email, with: ->(email) { email.to_s.strip.downcase.presence }
|
|
34
37
|
|
|
35
38
|
#
|
|
@@ -52,8 +55,15 @@ module Spree
|
|
|
52
55
|
Spree::CSV::NewsletterSubscriberPresenter.new(self).call
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
def self.subscribe(email:, user: nil)
|
|
56
|
-
Spree::
|
|
58
|
+
def self.subscribe(email:, user: nil, store: nil, redirect_url: nil)
|
|
59
|
+
store ||= Spree::Current.store
|
|
60
|
+
|
|
61
|
+
Spree::Newsletter::Subscribe.new(
|
|
62
|
+
email: email,
|
|
63
|
+
current_user: user,
|
|
64
|
+
current_store: store,
|
|
65
|
+
redirect_url: redirect_url
|
|
66
|
+
).call
|
|
57
67
|
end
|
|
58
68
|
|
|
59
69
|
def self.verify(token:)
|
|
@@ -61,5 +71,11 @@ module Spree
|
|
|
61
71
|
|
|
62
72
|
Spree::Newsletter::Verify.new(subscriber: subscriber).call
|
|
63
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def set_store
|
|
78
|
+
self.store ||= Spree::Current.store
|
|
79
|
+
end
|
|
64
80
|
end
|
|
65
81
|
end
|
|
@@ -27,7 +27,14 @@ module Spree
|
|
|
27
27
|
#
|
|
28
28
|
# Associations
|
|
29
29
|
with_options dependent: :destroy, inverse_of: :option_type do
|
|
30
|
-
|
|
30
|
+
# `autosave: true` makes the parent's `save`/`update`:
|
|
31
|
+
# - persist any built / mutated children in one transaction,
|
|
32
|
+
# - collect their validation errors onto `self.errors`,
|
|
33
|
+
# - destroy any child marked via `mark_for_destruction`.
|
|
34
|
+
# The custom `option_values=` writer below leans on this so the v3
|
|
35
|
+
# ResourceController gets `save returning false` + structured errors
|
|
36
|
+
# rather than raised exceptions.
|
|
37
|
+
has_many :option_values, -> { order(:position) }, autosave: true
|
|
31
38
|
has_many :product_option_types
|
|
32
39
|
end
|
|
33
40
|
has_many :products, through: :product_option_types
|
|
@@ -73,12 +80,6 @@ module Spree
|
|
|
73
80
|
after_update :touch_all_products, if: -> { saved_changes.key?(:presentation) }
|
|
74
81
|
after_destroy :touch_all_products
|
|
75
82
|
|
|
76
|
-
# legacy, name itself is now parameterized before saving
|
|
77
|
-
def filter_param
|
|
78
|
-
Spree::Deprecation.warn('Spree::OptionType#filter_param is deprecated and will be removed in Spree 5.5. Please use Spree::OptionType#name instead.')
|
|
79
|
-
name.parameterize
|
|
80
|
-
end
|
|
81
|
-
|
|
82
83
|
def self.color
|
|
83
84
|
colors.first
|
|
84
85
|
end
|
|
@@ -94,6 +95,46 @@ module Spree
|
|
|
94
95
|
color_swatch?
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Syncs option values from an array of hashes by mutating the in-memory
|
|
99
|
+
# `option_values` association — built/assigned children get persisted by
|
|
100
|
+
# `autosave: true` when the parent saves, and absent IDs get destroyed
|
|
101
|
+
# via `mark_for_destruction`. The single transaction is owned by the
|
|
102
|
+
# parent's `save`, so validation failures surface as `errors` and the
|
|
103
|
+
# whole thing rolls back together.
|
|
104
|
+
#
|
|
105
|
+
# Falls back to ActiveRecord's collection writer when given OptionValue
|
|
106
|
+
# records (e.g. from `accepts_nested_attributes_for` used by the legacy admin).
|
|
107
|
+
#
|
|
108
|
+
# @param option_values_params [Array<Hash>] array of option value attribute hashes
|
|
109
|
+
# @return [void]
|
|
110
|
+
def option_values=(option_values_params)
|
|
111
|
+
return super if option_values_params.blank? || option_values_params.first.is_a?(Spree::OptionValue)
|
|
112
|
+
|
|
113
|
+
# Load the association into the in-memory collection so subsequent
|
|
114
|
+
# `option_values.build` / `mark_for_destruction` mutations stay on the
|
|
115
|
+
# same instances `autosave` will traverse at parent-save time.
|
|
116
|
+
existing_by_id = option_values.to_a.index_by(&:id)
|
|
117
|
+
retained_ids = []
|
|
118
|
+
|
|
119
|
+
option_values_params.each do |value_data|
|
|
120
|
+
data = value_data.to_h.with_indifferent_access
|
|
121
|
+
value_id = data.delete(:id)
|
|
122
|
+
|
|
123
|
+
record = if value_id.present?
|
|
124
|
+
existing_by_id[Spree::PrefixedId.decode_prefixed_id(value_id) || value_id] ||
|
|
125
|
+
raise(ActiveRecord::RecordNotFound.new("Couldn't find Spree::OptionValue with param=#{value_id}", 'Spree::OptionValue'))
|
|
126
|
+
else
|
|
127
|
+
option_values.build
|
|
128
|
+
end
|
|
129
|
+
record.assign_attributes(data)
|
|
130
|
+
retained_ids << record.id if record.persisted?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
existing_by_id.each_value do |existing|
|
|
134
|
+
existing.mark_for_destruction unless retained_ids.include?(existing.id)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
97
138
|
private
|
|
98
139
|
|
|
99
140
|
def touch_all_products
|
|
@@ -135,7 +135,7 @@ module Spree
|
|
|
135
135
|
def subscribe_to_newsletter
|
|
136
136
|
return unless accept_marketing?
|
|
137
137
|
|
|
138
|
-
Spree::NewsletterSubscriber.subscribe(email: email, user: user)
|
|
138
|
+
Spree::NewsletterSubscriber.subscribe(email: email, user: user, store: store)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def self.go_to_state(name, options = {})
|
|
@@ -317,8 +317,8 @@ module Spree
|
|
|
317
317
|
# attributes for a single payment and its source, discarding attributes
|
|
318
318
|
# for payment methods other than the one selected
|
|
319
319
|
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
320
|
+
# If an existing credit card is provided, build the payment attributes
|
|
321
|
+
# from scratch so the amount can be set. Example payload:
|
|
322
322
|
#
|
|
323
323
|
# {
|
|
324
324
|
# "order": {
|
data/app/models/spree/order.rb
CHANGED
|
@@ -9,6 +9,11 @@ module Spree
|
|
|
9
9
|
class Order < Spree.base_class
|
|
10
10
|
has_prefix_id :or # Stripe: or_
|
|
11
11
|
|
|
12
|
+
# Legacy free-text `channel` column was replaced by the `channel_id` FK
|
|
13
|
+
# (see 6.0-order-routing.md). The string column stays in the DB so the
|
|
14
|
+
# 5.4-to-5.5 backfill rake can read it; AR ignores it everywhere else.
|
|
15
|
+
self.ignored_columns += ['channel']
|
|
16
|
+
|
|
12
17
|
PAYMENT_STATES = %w(balance_due credit_owed failed paid void)
|
|
13
18
|
SHIPMENT_STATES = %w(backorder canceled partial pending ready shipped)
|
|
14
19
|
LINE_ITEM_REMOVABLE_STATES = %w(cart address delivery payment confirm resumed)
|
|
@@ -104,23 +109,42 @@ module Spree
|
|
|
104
109
|
go_to_state :complete
|
|
105
110
|
end
|
|
106
111
|
|
|
107
|
-
self.whitelisted_ransackable_associations = %w[shipments user created_by approver canceler promotions bill_address ship_address line_items store]
|
|
112
|
+
self.whitelisted_ransackable_associations = %w[shipments user created_by approver canceler promotions bill_address ship_address line_items store channel tags]
|
|
108
113
|
self.whitelisted_ransackable_attributes = %w[
|
|
109
|
-
completed_at email number state payment_state shipment_state
|
|
110
|
-
total item_total item_count considered_risky
|
|
114
|
+
completed_at email number state status payment_state shipment_state
|
|
115
|
+
total item_total item_count considered_risky channel_id currency
|
|
111
116
|
]
|
|
112
|
-
self.whitelisted_ransackable_scopes = %w[refunded partially_refunded search multi_search]
|
|
117
|
+
self.whitelisted_ransackable_scopes = %w[complete incomplete refunded partially_refunded search multi_search]
|
|
113
118
|
|
|
114
119
|
attr_reader :coupon_code
|
|
115
120
|
attr_accessor :temporary_address
|
|
116
121
|
|
|
122
|
+
# Set to false on admin-initiated flows to suppress customer-facing emails.
|
|
123
|
+
attr_accessor :notify_customer
|
|
124
|
+
|
|
117
125
|
attribute :state_machine_resumed, :boolean
|
|
118
126
|
|
|
127
|
+
STATUSES = %w[draft placed canceled].freeze
|
|
128
|
+
|
|
129
|
+
attribute :status, :string, default: 'draft'
|
|
130
|
+
validates :status, inclusion: { in: STATUSES }
|
|
131
|
+
|
|
132
|
+
scope :drafts, -> { where(status: 'draft') }
|
|
133
|
+
scope :placed_orders, -> { where(status: 'placed') }
|
|
134
|
+
scope :canceled_orders, -> { where(status: 'canceled') }
|
|
135
|
+
|
|
119
136
|
acts_as_taggable_on :tags
|
|
120
137
|
acts_as_taggable_tenant :store_id
|
|
121
138
|
|
|
139
|
+
def tags=(tags)
|
|
140
|
+
self.tag_list = tags
|
|
141
|
+
end
|
|
142
|
+
|
|
122
143
|
ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :bill_address_id, :ship_address_id]
|
|
123
144
|
|
|
145
|
+
# 6.0 forward-compat: User→Customer rename. Column stays user_id in 5.x.
|
|
146
|
+
alias_attribute :customer_id, :user_id
|
|
147
|
+
|
|
124
148
|
belongs_to :user, class_name: "::#{Spree.user_class}", optional: true, autosave: true
|
|
125
149
|
belongs_to :created_by, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
126
150
|
belongs_to :approver, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
@@ -140,6 +164,8 @@ module Spree
|
|
|
140
164
|
|
|
141
165
|
belongs_to :store, class_name: 'Spree::Store'
|
|
142
166
|
belongs_to :market, class_name: 'Spree::Market', optional: true
|
|
167
|
+
belongs_to :channel, class_name: 'Spree::Channel', optional: true
|
|
168
|
+
belongs_to :preferred_stock_location, class_name: 'Spree::StockLocation', optional: true
|
|
143
169
|
|
|
144
170
|
with_options dependent: :destroy do
|
|
145
171
|
has_many :state_changes, as: :stateful, class_name: 'Spree::StateChange'
|
|
@@ -148,11 +174,14 @@ module Spree
|
|
|
148
174
|
has_many :payment_sessions, inverse_of: :order, class_name: 'Spree::PaymentSession'
|
|
149
175
|
has_many :return_authorizations, inverse_of: :order, class_name: 'Spree::ReturnAuthorization'
|
|
150
176
|
has_many :adjustments, -> { order(:created_at) }, as: :adjustable, class_name: 'Spree::Adjustment'
|
|
177
|
+
has_many :cancellations, -> { order(:created_at) }, inverse_of: :order, class_name: 'Spree::OrderCancellation'
|
|
178
|
+
has_many :approvals, -> { order(:created_at) }, inverse_of: :order, class_name: 'Spree::OrderApproval'
|
|
151
179
|
end
|
|
152
180
|
has_many :reimbursements, inverse_of: :order, class_name: 'Spree::Reimbursement'
|
|
153
181
|
has_many :customer_returns, class_name: 'Spree::CustomerReturn', through: :return_authorizations
|
|
154
182
|
has_many :line_item_adjustments, through: :line_items, source: :adjustments
|
|
155
183
|
has_many :inventory_units, inverse_of: :order, class_name: 'Spree::InventoryUnit'
|
|
184
|
+
has_many :stock_reservations, class_name: 'Spree::StockReservation', inverse_of: :order, dependent: :destroy
|
|
156
185
|
has_many :return_items, through: :inventory_units, class_name: 'Spree::ReturnItem'
|
|
157
186
|
has_many :variants, through: :line_items
|
|
158
187
|
has_many :products, through: :variants
|
|
@@ -194,6 +223,7 @@ module Spree
|
|
|
194
223
|
# Needs to happen before save_permalink is called
|
|
195
224
|
before_validation :ensure_store_presence
|
|
196
225
|
before_validation :ensure_market_presence
|
|
226
|
+
before_validation :ensure_channel_presence
|
|
197
227
|
before_validation :ensure_currency_presence
|
|
198
228
|
before_validation :ensure_locale_presence
|
|
199
229
|
before_validation :resolve_market_from_currency, if: -> { persisted? && currency_changed? && !skip_market_resolution }
|
|
@@ -348,6 +378,21 @@ module Spree
|
|
|
348
378
|
completed_at.present?
|
|
349
379
|
end
|
|
350
380
|
|
|
381
|
+
# True when the order is mid-checkout: past the `cart` state but not yet
|
|
382
|
+
# completed or canceled. Used by stock reservation hooks and any flow
|
|
383
|
+
# that should only run during the active checkout phase.
|
|
384
|
+
def in_checkout?
|
|
385
|
+
!cart? && !complete? && !canceled?
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def draft?
|
|
389
|
+
status == 'draft'
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def placed?
|
|
393
|
+
status == 'placed'
|
|
394
|
+
end
|
|
395
|
+
|
|
351
396
|
# Checks if the order is fully refunded
|
|
352
397
|
# @return [Boolean]
|
|
353
398
|
def order_refunded?
|
|
@@ -459,6 +504,12 @@ module Spree
|
|
|
459
504
|
self.market ||= Spree::Current.market || store&.default_market
|
|
460
505
|
end
|
|
461
506
|
|
|
507
|
+
def ensure_channel_presence
|
|
508
|
+
return if channel_id.present?
|
|
509
|
+
|
|
510
|
+
self.channel = store&.default_channel
|
|
511
|
+
end
|
|
512
|
+
|
|
462
513
|
def allow_cancel?
|
|
463
514
|
return false if !completed? || canceled?
|
|
464
515
|
|
|
@@ -591,6 +642,7 @@ module Spree
|
|
|
591
642
|
end
|
|
592
643
|
|
|
593
644
|
updater.update_shipment_state
|
|
645
|
+
self.status = 'placed'
|
|
594
646
|
save!
|
|
595
647
|
updater.run_hooks
|
|
596
648
|
|
|
@@ -719,7 +771,34 @@ module Spree
|
|
|
719
771
|
# and are not returned or shipped should be deleted
|
|
720
772
|
inventory_units.on_hand_or_backordered.delete_all
|
|
721
773
|
|
|
722
|
-
self.shipments =
|
|
774
|
+
self.shipments = order_routing_strategy.for_allocation.map do |package|
|
|
775
|
+
package.to_shipment.tap { |s| s.address_id = ship_address_id }
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Resolves the routing strategy from the channel override first, then the
|
|
780
|
+
# store default. Only a registered Spree::OrderRouting::Strategy::Base
|
|
781
|
+
# subclass is used; any other value (an unregistered/typo'd class, or a
|
|
782
|
+
# strategy that was unregistered after being persisted) is logged and
|
|
783
|
+
# skipped rather than raised, falling back to the default Rules strategy so
|
|
784
|
+
# a misconfiguration can't take down cart display or checkout.
|
|
785
|
+
#
|
|
786
|
+
# @return [Spree::OrderRouting::Strategy::Base]
|
|
787
|
+
def order_routing_strategy
|
|
788
|
+
klass = valid_order_routing_strategy_class(channel&.preferred_order_routing_strategy) ||
|
|
789
|
+
valid_order_routing_strategy_class(store.preferred_order_routing_strategy) ||
|
|
790
|
+
Spree::OrderRouting::Strategy::Rules
|
|
791
|
+
|
|
792
|
+
klass.new(order: self)
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Cascade for the `preferred_location` rule kind. Channel and B2B sources
|
|
796
|
+
# are layered in by their respective plans.
|
|
797
|
+
#
|
|
798
|
+
# @return [Integer, nil]
|
|
799
|
+
def inferred_preferred_stock_location_id
|
|
800
|
+
preferred_stock_location_id.presence ||
|
|
801
|
+
created_by&.try(:preferred_stock_location_id)
|
|
723
802
|
end
|
|
724
803
|
|
|
725
804
|
# Returns the total weight of the inventory units in the order
|
|
@@ -965,6 +1044,19 @@ module Spree
|
|
|
965
1044
|
|
|
966
1045
|
private
|
|
967
1046
|
|
|
1047
|
+
def valid_order_routing_strategy_class(klass_name)
|
|
1048
|
+
return if klass_name.blank?
|
|
1049
|
+
|
|
1050
|
+
klass = Spree.order_routing.strategies.find { |strategy| strategy.to_s == klass_name.to_s }
|
|
1051
|
+
return klass if klass
|
|
1052
|
+
|
|
1053
|
+
Rails.logger.warn(
|
|
1054
|
+
"[Spree] Ignoring unregistered order routing strategy #{klass_name.inspect} " \
|
|
1055
|
+
"for order #{number.inspect}; falling back to the default strategy."
|
|
1056
|
+
)
|
|
1057
|
+
nil
|
|
1058
|
+
end
|
|
1059
|
+
|
|
968
1060
|
def link_by_email
|
|
969
1061
|
self.email = user.email if user
|
|
970
1062
|
end
|
|
@@ -998,6 +1090,8 @@ module Spree
|
|
|
998
1090
|
end
|
|
999
1091
|
|
|
1000
1092
|
def after_cancel
|
|
1093
|
+
update_column(:status, 'canceled')
|
|
1094
|
+
|
|
1001
1095
|
shipments.each(&:cancel!)
|
|
1002
1096
|
|
|
1003
1097
|
# payments fully covered by gift card won't be refunded
|
|
@@ -1015,6 +1109,8 @@ module Spree
|
|
|
1015
1109
|
end
|
|
1016
1110
|
|
|
1017
1111
|
def after_resume
|
|
1112
|
+
update_column(:status, 'placed')
|
|
1113
|
+
|
|
1018
1114
|
shipments.each(&:resume!)
|
|
1019
1115
|
consider_risk
|
|
1020
1116
|
send_order_resumed_webhook
|
|
@@ -1089,7 +1185,7 @@ module Spree
|
|
|
1089
1185
|
end
|
|
1090
1186
|
|
|
1091
1187
|
def publish_order_completed_event
|
|
1092
|
-
publish_event('order.completed')
|
|
1188
|
+
publish_event('order.completed', event_payload.merge(notify_customer: notify_customer))
|
|
1093
1189
|
end
|
|
1094
1190
|
|
|
1095
1191
|
def publish_order_resumed_event
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class OrderApproval < Spree.base_class
|
|
3
|
+
has_prefix_id :appr
|
|
4
|
+
|
|
5
|
+
STATUSES = %w[pending approved rejected].freeze
|
|
6
|
+
|
|
7
|
+
attribute :metadata, default: -> { {} }
|
|
8
|
+
|
|
9
|
+
belongs_to :order, class_name: 'Spree::Order', inverse_of: :approvals
|
|
10
|
+
belongs_to :approver, polymorphic: true, optional: true
|
|
11
|
+
|
|
12
|
+
validates :order, presence: true
|
|
13
|
+
validates :status, presence: true, inclusion: { in: STATUSES }
|
|
14
|
+
|
|
15
|
+
scope :approved, -> { where(status: 'approved') }
|
|
16
|
+
scope :pending, -> { where(status: 'pending') }
|
|
17
|
+
scope :rejected, -> { where(status: 'rejected') }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class OrderCancellation < Spree.base_class
|
|
3
|
+
has_prefix_id :cncl
|
|
4
|
+
|
|
5
|
+
REASONS = %w[customer declined fraud inventory staff other expired].freeze
|
|
6
|
+
|
|
7
|
+
attribute :restock_items, :boolean, default: false
|
|
8
|
+
attribute :refund_payments, :boolean, default: false
|
|
9
|
+
attribute :notify_customer, :boolean, default: false
|
|
10
|
+
attribute :metadata, default: -> { {} }
|
|
11
|
+
|
|
12
|
+
belongs_to :order, class_name: 'Spree::Order', inverse_of: :cancellations
|
|
13
|
+
belongs_to :canceled_by, polymorphic: true, optional: true
|
|
14
|
+
|
|
15
|
+
validates :order, presence: true
|
|
16
|
+
validates :reason, presence: true, inclusion: { in: REASONS }
|
|
17
|
+
validates :refund_amount, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
# Shared validation for models carrying a +preferred_order_routing_strategy+
|
|
4
|
+
# preference (Spree::Store, Spree::Channel). A blank value is allowed (it
|
|
5
|
+
# falls back to the next level / the default Rules strategy); a present value
|
|
6
|
+
# must name a registered Spree::OrderRouting::Strategy::Base subclass.
|
|
7
|
+
module HasStrategyPreference
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
validate :order_routing_strategy_must_be_registered
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def order_routing_strategy_must_be_registered
|
|
17
|
+
value = preferred_order_routing_strategy
|
|
18
|
+
return if value.blank?
|
|
19
|
+
return if Spree.order_routing.strategies.any? { |strategy| strategy.to_s == value.to_s }
|
|
20
|
+
|
|
21
|
+
errors.add(
|
|
22
|
+
:preferred_order_routing_strategy,
|
|
23
|
+
Spree.t(:invalid_order_routing_strategy, scope: [:errors, :messages], default: 'is not a registered order routing strategy')
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
module Rules
|
|
4
|
+
# Ranks the StockLocation marked `default: true` at 0 and others at 1.
|
|
5
|
+
# Provides a deterministic baseline so the reducer always has a winner
|
|
6
|
+
# once higher-priority rules abstain or tie.
|
|
7
|
+
class DefaultLocation < Spree::OrderRoutingRule
|
|
8
|
+
def rank(_order, locations)
|
|
9
|
+
locations.map do |loc|
|
|
10
|
+
LocationRanking.new(location: loc, rank: loc.default? ? 0 : 1)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
module Rules
|
|
4
|
+
# Prefers locations that can fulfill more demand on their own.
|
|
5
|
+
# Higher coverage → lower (better) rank, so the location that single-handedly
|
|
6
|
+
# covers the most variants wins. Coverage is counted per distinct variant
|
|
7
|
+
# so a variant repeated across multiple line items isn't double-counted.
|
|
8
|
+
class MinimizeSplits < Spree::OrderRoutingRule
|
|
9
|
+
def rank(order, locations)
|
|
10
|
+
demand = required_quantity_by_variant(order)
|
|
11
|
+
counts = stock_item_counts(demand.keys, locations)
|
|
12
|
+
|
|
13
|
+
locations.map do |loc|
|
|
14
|
+
coverage = demand.count do |variant_id, qty|
|
|
15
|
+
(counts[[loc.id, variant_id]] || 0) >= qty
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
LocationRanking.new(location: loc, rank: -coverage)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def required_quantity_by_variant(order)
|
|
25
|
+
order.line_items.each_with_object(Hash.new(0)) do |li, h|
|
|
26
|
+
next if li.variant_id.nil?
|
|
27
|
+
|
|
28
|
+
h[li.variant_id] += li.quantity
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# One query for the entire location × variant matrix instead of
|
|
33
|
+
# N variants × M locations stock_item lookups.
|
|
34
|
+
def stock_item_counts(variant_ids, locations)
|
|
35
|
+
return {} if variant_ids.empty? || locations.empty?
|
|
36
|
+
|
|
37
|
+
Spree::StockItem
|
|
38
|
+
.where(stock_location_id: locations.map(&:id), variant_id: variant_ids)
|
|
39
|
+
.pluck(:stock_location_id, :variant_id, :count_on_hand)
|
|
40
|
+
.each_with_object({}) { |(loc_id, var_id, count), h| h[[loc_id, var_id]] = count }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
module Rules
|
|
4
|
+
# Ranks the order's inferred preferred location at 0 and abstains for
|
|
5
|
+
# everything else. Lets admins / staff / B2B contexts pin "fulfill from
|
|
6
|
+
# this location" without preventing fallback when the preferred location
|
|
7
|
+
# doesn't actually stock the items — subsequent rules tie-break.
|
|
8
|
+
class PreferredLocation < Spree::OrderRoutingRule
|
|
9
|
+
def rank(order, locations)
|
|
10
|
+
preferred_id = order.inferred_preferred_stock_location_id
|
|
11
|
+
|
|
12
|
+
locations.map do |loc|
|
|
13
|
+
LocationRanking.new(
|
|
14
|
+
location: loc,
|
|
15
|
+
rank: (preferred_id.present? && loc.id.to_s == preferred_id.to_s) ? 0 : nil
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
module Strategy
|
|
4
|
+
# Contract for order routing strategies. Subclasses implement all four
|
|
5
|
+
# methods — there are no defaults. New routing *signals* (proximity,
|
|
6
|
+
# day-of-week, etc.) ship as STI subclasses of Spree::OrderRoutingRule;
|
|
7
|
+
# a custom strategy is appropriate only when the algorithm itself is a
|
|
8
|
+
# different shape (OMS delegation, ML model, optimization solver).
|
|
9
|
+
#
|
|
10
|
+
# Selected per Order via Spree::Order#order_routing_strategy.
|
|
11
|
+
# See docs/plans/6.0-order-routing.md.
|
|
12
|
+
class Base
|
|
13
|
+
attr_reader :order
|
|
14
|
+
|
|
15
|
+
# Human label for admin strategy pickers. Override in a subclass or add
|
|
16
|
+
# an i18n key under +spree.order_routing.strategies+.
|
|
17
|
+
#
|
|
18
|
+
# @return [String]
|
|
19
|
+
def self.display_name
|
|
20
|
+
Spree.t(name.demodulize.underscore, scope: 'order_routing.strategies', default: name.demodulize.titleize)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(order:)
|
|
24
|
+
@order = order
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Array<Spree::Stock::Package>]
|
|
28
|
+
def for_allocation
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #for_allocation"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param fulfillment [Spree::Shipment]
|
|
33
|
+
def for_sale(fulfillment:)
|
|
34
|
+
raise NotImplementedError, "#{self.class} must implement #for_sale"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def for_release
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #for_release"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def for_cancellation
|
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #for_cancellation"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderRouting
|
|
3
|
+
module Strategy
|
|
4
|
+
# Pre-5.5 routing behavior. Delegates to Spree::Stock::Coordinator,
|
|
5
|
+
# which packs every active stock location and lets Prioritizer's
|
|
6
|
+
# Adjuster distribute units across the resulting packages — no rules
|
|
7
|
+
# consulted, no merchant-driven preferences, location order is
|
|
8
|
+
# whatever the database returns.
|
|
9
|
+
#
|
|
10
|
+
# Provided as an opt-in escape hatch for merchants upgrading from 5.4
|
|
11
|
+
# who are not ready to adopt rules-based routing. Configure via:
|
|
12
|
+
#
|
|
13
|
+
# store.update!(preferred_order_routing_strategy: 'Spree::OrderRouting::Strategy::Legacy')
|
|
14
|
+
#
|
|
15
|
+
# Spree 6.0 drops this strategy along with the underlying Coordinator.
|
|
16
|
+
# See docs/plans/6.0-order-routing.md.
|
|
17
|
+
class Legacy < Base
|
|
18
|
+
def for_allocation
|
|
19
|
+
Spree::Stock::Coordinator.new(order).packages
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Stock decrement / restock today happens via Spree::Shipment's state
|
|
23
|
+
# machine (after_ship / after_cancel). The strategy hooks below are
|
|
24
|
+
# part of the contract for the future reservation + typed-movement
|
|
25
|
+
# phase. In 5.5 they are no-ops; existing model callbacks already do
|
|
26
|
+
# the right thing.
|
|
27
|
+
def for_sale(fulfillment:); end
|
|
28
|
+
def for_release; end
|
|
29
|
+
def for_cancellation; end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module OrderRouting
|
|
5
|
+
module Strategy
|
|
6
|
+
# Walks rules in priority order and applies a "first non-tie wins" reducer.
|
|
7
|
+
#
|
|
8
|
+
# For each rule:
|
|
9
|
+
# 1. Drop rankings where rank is nil (rule abstains for that location).
|
|
10
|
+
# 2. Find the location(s) with the lowest rank (best).
|
|
11
|
+
# 3. Unique winner -> return it.
|
|
12
|
+
# 4. Tie -> carry the tied set forward to the next rule.
|
|
13
|
+
#
|
|
14
|
+
# Out of rules with ties: prefer the StockLocation marked default,
|
|
15
|
+
# then by id. Guarantees a winner whenever locations is non-empty.
|
|
16
|
+
class Reducer
|
|
17
|
+
def initialize(rules, order:)
|
|
18
|
+
@rules = rules
|
|
19
|
+
@order = order
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param locations [Array<Spree::StockLocation>]
|
|
23
|
+
# @return [Spree::StockLocation, nil]
|
|
24
|
+
def pick(locations)
|
|
25
|
+
return nil if locations.empty?
|
|
26
|
+
|
|
27
|
+
remaining = locations
|
|
28
|
+
remaining_ids = remaining.map(&:id).to_set
|
|
29
|
+
|
|
30
|
+
@rules.each do |rule|
|
|
31
|
+
rankings = rule.rank(@order, remaining).select do |r|
|
|
32
|
+
r.rank && remaining_ids.include?(r.location.id)
|
|
33
|
+
end
|
|
34
|
+
next if rankings.empty?
|
|
35
|
+
|
|
36
|
+
min_rank = rankings.map(&:rank).min
|
|
37
|
+
top = rankings.select { |r| r.rank == min_rank }.map(&:location)
|
|
38
|
+
|
|
39
|
+
return top.first if top.size == 1
|
|
40
|
+
|
|
41
|
+
remaining = top
|
|
42
|
+
remaining_ids = top.map(&:id).to_set
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
remaining.min_by { |l| [l.default? ? 0 : 1, l.id] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns every input location, ordered best-first by the same rule
|
|
49
|
+
# chain that drives #pick. Each successive location is the best of
|
|
50
|
+
# what remains — used by Strategy::Rules to fan out an allocation
|
|
51
|
+
# across multiple locations when no single location covers the cart.
|
|
52
|
+
#
|
|
53
|
+
# @param locations [Array<Spree::StockLocation>]
|
|
54
|
+
# @return [Array<Spree::StockLocation>]
|
|
55
|
+
def rank_all(locations)
|
|
56
|
+
remaining = locations.dup
|
|
57
|
+
ordered = []
|
|
58
|
+
until remaining.empty?
|
|
59
|
+
chosen = pick(remaining) or break
|
|
60
|
+
ordered << chosen
|
|
61
|
+
remaining = remaining.reject { |l| l.id == chosen.id }
|
|
62
|
+
end
|
|
63
|
+
ordered
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|