spree_core 5.4.2 → 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.
Files changed (248) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +0 -82
  3. data/app/helpers/spree/currency_helper.rb +0 -12
  4. data/app/helpers/spree/products_helper.rb +0 -8
  5. data/app/jobs/spree/base_job.rb +18 -0
  6. data/app/jobs/spree/events/subscriber_job.rb +3 -2
  7. data/app/jobs/spree/exports/generate_job.rb +11 -0
  8. data/app/jobs/spree/images/save_from_url_job.rb +23 -8
  9. data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
  10. data/app/jobs/spree/imports/base_job.rb +15 -0
  11. data/app/jobs/spree/imports/create_categories_job.rb +37 -0
  12. data/app/jobs/spree/imports/create_rows_job.rb +1 -3
  13. data/app/jobs/spree/imports/process_group_job.rb +8 -6
  14. data/app/jobs/spree/imports/process_rows_job.rb +1 -3
  15. data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
  16. data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
  17. data/app/jobs/spree/reports/generate_job.rb +11 -0
  18. data/app/jobs/spree/search_provider/index_job.rb +5 -1
  19. data/app/jobs/spree/search_provider/remove_job.rb +4 -0
  20. data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
  21. data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
  22. data/app/models/concerns/spree/display_on.rb +31 -0
  23. data/app/models/concerns/spree/metafields.rb +167 -5
  24. data/app/models/concerns/spree/preference_schema.rb +191 -0
  25. data/app/models/concerns/spree/prefixed_id.rb +94 -11
  26. data/app/models/concerns/spree/product_scopes.rb +36 -17
  27. data/app/models/concerns/spree/publishable.rb +1 -1
  28. data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
  29. data/app/models/concerns/spree/search_indexable.rb +8 -7
  30. data/app/models/concerns/spree/searchable.rb +11 -2
  31. data/app/models/concerns/spree/stores/channels.rb +20 -0
  32. data/app/models/concerns/spree/stores/markets.rb +21 -5
  33. data/app/models/concerns/spree/typed_associations.rb +120 -0
  34. data/app/models/concerns/spree/user_methods.rb +71 -12
  35. data/app/models/spree/ability.rb +4 -117
  36. data/app/models/spree/api_key.rb +53 -0
  37. data/app/models/spree/asset.rb +37 -14
  38. data/app/models/spree/authentication/strategy_registry.rb +72 -0
  39. data/app/models/spree/base.rb +18 -1
  40. data/app/models/spree/channel.rb +159 -0
  41. data/app/models/spree/country.rb +2 -0
  42. data/app/models/spree/current.rb +5 -1
  43. data/app/models/spree/custom_field.rb +9 -0
  44. data/app/models/spree/custom_field_definition.rb +7 -0
  45. data/app/models/spree/customer_group.rb +8 -2
  46. data/app/models/spree/event.rb +6 -6
  47. data/app/models/spree/export.rb +32 -5
  48. data/app/models/spree/exports/product_translations.rb +1 -1
  49. data/app/models/spree/gateway/bogus.rb +6 -1
  50. data/app/models/spree/gateway.rb +25 -0
  51. data/app/models/spree/gift_card.rb +1 -1
  52. data/app/models/spree/gift_card_batch.rb +4 -1
  53. data/app/models/spree/import.rb +5 -0
  54. data/app/models/spree/import_row.rb +12 -0
  55. data/app/models/spree/line_item.rb +7 -2
  56. data/app/models/spree/market.rb +57 -1
  57. data/app/models/spree/metafield.rb +38 -0
  58. data/app/models/spree/metafield_definition.rb +29 -6
  59. data/app/models/spree/metafields/json.rb +10 -0
  60. data/app/models/spree/newsletter_subscriber.rb +19 -3
  61. data/app/models/spree/option_type.rb +48 -7
  62. data/app/models/spree/order/checkout.rb +3 -3
  63. data/app/models/spree/order.rb +102 -6
  64. data/app/models/spree/order_approval.rb +19 -0
  65. data/app/models/spree/order_cancellation.rb +19 -0
  66. data/app/models/spree/order_inventory.rb +24 -2
  67. data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
  68. data/app/models/spree/order_routing/rules/default_location.rb +16 -0
  69. data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
  70. data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
  71. data/app/models/spree/order_routing/strategy/base.rb +47 -0
  72. data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
  73. data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
  74. data/app/models/spree/order_routing/strategy/rules.rb +81 -0
  75. data/app/models/spree/order_routing_rule.rb +75 -0
  76. data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
  77. data/app/models/spree/permission_sets/configuration_management.rb +16 -0
  78. data/app/models/spree/permission_sets/product_display.rb +2 -0
  79. data/app/models/spree/permission_sets/product_management.rb +2 -0
  80. data/app/models/spree/price.rb +14 -1
  81. data/app/models/spree/price_list.rb +129 -17
  82. data/app/models/spree/price_rule.rb +11 -1
  83. data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
  84. data/app/models/spree/price_rules/market_rule.rb +16 -1
  85. data/app/models/spree/price_rules/user_rule.rb +21 -2
  86. data/app/models/spree/product/channels.rb +149 -0
  87. data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
  88. data/app/models/spree/product/slugs.rb +1 -1
  89. data/app/models/spree/product.rb +172 -31
  90. data/app/models/spree/product_publication.rb +43 -0
  91. data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
  92. data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
  93. data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
  94. data/app/models/spree/promotion/rules/country.rb +40 -18
  95. data/app/models/spree/promotion/rules/customer_group.rb +10 -1
  96. data/app/models/spree/promotion/rules/product.rb +4 -0
  97. data/app/models/spree/promotion/rules/taxon.rb +24 -1
  98. data/app/models/spree/promotion/rules/user.rb +21 -0
  99. data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
  100. data/app/models/spree/promotion.rb +22 -1
  101. data/app/models/spree/promotion_action.rb +17 -11
  102. data/app/models/spree/promotion_rule.rb +17 -18
  103. data/app/models/spree/search_provider/meilisearch.rb +12 -2
  104. data/app/models/spree/shipment.rb +10 -4
  105. data/app/models/spree/stock/availability_validator.rb +1 -1
  106. data/app/models/spree/stock/quantifier.rb +89 -9
  107. data/app/models/spree/stock_item.rb +36 -0
  108. data/app/models/spree/stock_location.rb +52 -0
  109. data/app/models/spree/stock_reservation.rb +38 -0
  110. data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
  111. data/app/models/spree/store.rb +18 -72
  112. data/app/models/spree/store_credit.rb +0 -8
  113. data/app/models/spree/store_product.rb +11 -23
  114. data/app/models/spree/subscriber.rb +12 -12
  115. data/app/models/spree/taxon.rb +0 -5
  116. data/app/models/spree/user_identity.rb +1 -2
  117. data/app/models/spree/variant.rb +132 -18
  118. data/app/models/spree/variant_media.rb +46 -0
  119. data/app/models/spree/webhook_delivery.rb +1 -1
  120. data/app/models/spree/webhook_endpoint.rb +24 -0
  121. data/app/models/spree/wished_item.rb +0 -13
  122. data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
  123. data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
  124. data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
  125. data/app/presenters/spree/variant_presenter.rb +4 -3
  126. data/app/services/spree/addresses/update.rb +6 -8
  127. data/app/services/spree/cart/add_item.rb +10 -0
  128. data/app/services/spree/cart/empty.rb +2 -0
  129. data/app/services/spree/cart/remove_line_item.rb +10 -0
  130. data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
  131. data/app/services/spree/cart/set_quantity.rb +10 -0
  132. data/app/services/spree/carts/complete.rb +1 -0
  133. data/app/services/spree/carts/create.rb +1 -0
  134. data/app/services/spree/carts/update.rb +18 -2
  135. data/app/services/spree/carts/upsert_items.rb +6 -6
  136. data/app/services/spree/credit_cards/destroy.rb +1 -1
  137. data/app/services/spree/imports/row_processors/customer.rb +4 -1
  138. data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
  139. data/app/services/spree/newsletter/link_user.rb +53 -0
  140. data/app/services/spree/newsletter/subscribe.rb +31 -9
  141. data/app/services/spree/orders/approve.rb +27 -6
  142. data/app/services/spree/orders/build_shipments.rb +29 -0
  143. data/app/services/spree/orders/cancel.rb +34 -3
  144. data/app/services/spree/orders/complete.rb +53 -0
  145. data/app/services/spree/orders/create.rb +156 -0
  146. data/app/services/spree/orders/update.rb +51 -0
  147. data/app/services/spree/orders/upsert_items.rb +70 -0
  148. data/app/services/spree/payments/handle_webhook.rb +3 -10
  149. data/app/services/spree/prices/bulk_upsert.rb +201 -0
  150. data/app/services/spree/products/duplicator.rb +1 -1
  151. data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
  152. data/app/services/spree/sample_data/loader.rb +30 -0
  153. data/app/services/spree/stock_reservations/extend.rb +19 -0
  154. data/app/services/spree/stock_reservations/release.rb +12 -0
  155. data/app/services/spree/stock_reservations/reserve.rb +103 -0
  156. data/app/services/spree/taxons/remove_products.rb +7 -1
  157. data/app/subscribers/spree/event_log_subscriber.rb +1 -1
  158. data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
  159. data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
  160. data/config/locales/en.yml +35 -10
  161. data/config/routes.rb +9 -0
  162. data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
  163. data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
  164. data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
  165. data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
  166. data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
  167. data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
  168. data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
  169. data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
  170. data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
  171. data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
  172. data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
  173. data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
  174. data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
  175. data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
  176. data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
  177. data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
  178. data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
  179. data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
  180. data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
  181. data/db/sample_data/channels.rb +12 -0
  182. data/db/sample_data/orders.rb +1 -1
  183. data/db/sample_data/products.csv +212 -212
  184. data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
  185. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
  186. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
  187. data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
  188. data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
  189. data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
  190. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
  191. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
  192. data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
  193. data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
  194. data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
  195. data/lib/generators/spree/model/model_generator.rb +73 -7
  196. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
  197. data/lib/generators/spree/model/templates/model.rb.tt +28 -2
  198. data/lib/spree/core/configuration.rb +7 -0
  199. data/lib/spree/core/controller_helpers/auth.rb +0 -12
  200. data/lib/spree/core/controller_helpers/currency.rb +0 -17
  201. data/lib/spree/core/controller_helpers/order.rb +0 -19
  202. data/lib/spree/core/dependencies.rb +5 -2
  203. data/lib/spree/core/engine.rb +54 -7
  204. data/lib/spree/core/permission_configuration.rb +15 -0
  205. data/lib/spree/core/preferences/masking.rb +47 -0
  206. data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
  207. data/lib/spree/core/version.rb +1 -1
  208. data/lib/spree/core.rb +56 -5
  209. data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
  210. data/lib/spree/events/adapters/base.rb +3 -3
  211. data/lib/spree/events/registry.rb +1 -1
  212. data/lib/spree/events.rb +1 -1
  213. data/lib/spree/permitted_attributes.rb +9 -7
  214. data/lib/spree/testing_support/factories/address_factory.rb +16 -9
  215. data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
  216. data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
  217. data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
  218. data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
  219. data/lib/spree/testing_support/factories/product_factory.rb +16 -7
  220. data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
  221. data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
  222. data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
  223. data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
  224. data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
  225. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  226. data/lib/spree/testing_support/store.rb +10 -0
  227. data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
  228. data/lib/tasks/channels.rake +94 -0
  229. data/lib/tasks/core.rake +1 -0
  230. data/lib/tasks/media.rake +27 -0
  231. data/lib/tasks/products.rake +4 -6
  232. data/lib/tasks/publications.rake +60 -0
  233. data/lib/tasks/upgrade.rake +211 -0
  234. metadata +86 -18
  235. data/app/finders/spree/variants/visible_finder.rb +0 -23
  236. data/app/paginators/spree/shared/paginate.rb +0 -30
  237. data/app/presenters/spree/filters/price_presenter.rb +0 -23
  238. data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
  239. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
  240. data/app/presenters/spree/product_summary_presenter.rb +0 -27
  241. data/app/presenters/spree/variants/options_presenter.rb +0 -82
  242. data/app/services/spree/classifications/reposition.rb +0 -23
  243. data/app/sorters/spree/orders/sort.rb +0 -10
  244. data/lib/spree/core/controller_helpers/common.rb +0 -14
  245. data/lib/spree/core/token_generator.rb +0 -23
  246. data/lib/spree/database_type_utilities.rb +0 -22
  247. data/lib/spree/testing_support/bar_ability.rb +0 -14
  248. data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
@@ -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
@@ -0,0 +1,81 @@
1
+ module Spree
2
+ module OrderRouting
3
+ module Strategy
4
+ # Default order routing strategy: walks Spree::OrderRoutingRule rows in
5
+ # priority order, runs the Reducer to fully rank eligible locations,
6
+ # packs each location, and lets Spree::Stock::Prioritizer distribute
7
+ # inventory units across packages so units that the top-ranked
8
+ # location can't cover spill over to subsequent locations.
9
+ #
10
+ # See docs/plans/6.0-order-routing.md.
11
+ class Rules < Base
12
+ def for_allocation
13
+ locations = eligible_locations
14
+ return [] if locations.empty?
15
+
16
+ ordered = Reducer.new(applicable_rules.to_a, order: order).rank_all(locations)
17
+ return [] if ordered.empty?
18
+
19
+ packages = build_packages(ordered)
20
+ packages = prioritize_packages(packages)
21
+ estimate_rates(packages)
22
+ end
23
+
24
+ # Stock decrement / restock today happens via Spree::Shipment's state
25
+ # machine (after_ship / after_cancel). The strategy methods below are
26
+ # part of the contract for the future reservation + typed-movement
27
+ # phase — see 6.0-stock-reservations.md and 6.0-typed-stock-movements.md.
28
+ # In 5.5 they are no-ops; the existing model callbacks already do the
29
+ # right thing.
30
+ def for_sale(fulfillment:); end
31
+ def for_release; end
32
+ def for_cancellation; end
33
+
34
+ private
35
+
36
+ def applicable_rules
37
+ order.channel.order_routing_rules.active.ordered
38
+ end
39
+
40
+ def eligible_locations
41
+ Spree::StockLocation.active
42
+ .joins(:stock_items)
43
+ .where(spree_stock_items: { variant_id: requested_variant_ids })
44
+ .distinct
45
+ .to_a
46
+ end
47
+
48
+ def requested_variant_ids
49
+ inventory_units.map(&:variant_id).uniq
50
+ end
51
+
52
+ def inventory_units
53
+ @inventory_units ||= Spree::Stock::InventoryUnitBuilder.new(order).units
54
+ end
55
+
56
+ # Pack each ranked location independently. Packages are emitted in
57
+ # rank order so the Prioritizer's first-package-wins-on-hand logic
58
+ # honors the routing decision.
59
+ def build_packages(locations)
60
+ locations.flat_map do |location|
61
+ Spree::Stock::Packer.new(location, inventory_units, Spree.stock_splitters).packages
62
+ end
63
+ end
64
+
65
+ # Prioritizer's Adjuster distributes each inventory_unit across
66
+ # packages: the first package with on-hand stock fulfills the unit,
67
+ # and downstream packages have that unit removed. Packages whose
68
+ # items all get stripped are pruned.
69
+ def prioritize_packages(packages)
70
+ Spree::Stock::Prioritizer.new(packages).prioritized_packages
71
+ end
72
+
73
+ def estimate_rates(packages)
74
+ estimator = Spree::Stock::Estimator.new(order)
75
+ packages.each { |pkg| pkg.shipping_rates = estimator.shipping_rates(pkg) }
76
+ packages
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,75 @@
1
+ module Spree
2
+ # STI base for order routing rules. Subclasses live in
3
+ # app/models/spree/order_routing/rules/ and implement #rank(order, locations).
4
+ #
5
+ # Plugins extend the engine by defining a new subclass:
6
+ #
7
+ # class AcmeFresh::OrderRouting::RefrigeratedRule < Spree::OrderRoutingRule
8
+ # preference :max_temp_c, :integer, default: 4
9
+ #
10
+ # def rank(order, locations)
11
+ # # ... return Array<LocationRanking>
12
+ # end
13
+ # end
14
+ #
15
+ # See docs/plans/6.0-order-routing.md.
16
+ class OrderRoutingRule < Spree.base_class
17
+ self.table_name = 'spree_order_routing_rules'
18
+
19
+ # `rank` is integer (lower = better) when the rule has an opinion,
20
+ # nil to abstain (the reducer skips abstaining rankings).
21
+ LocationRanking = Struct.new(:location, :rank, keyword_init: true)
22
+
23
+ has_prefix_id :orule
24
+
25
+ include Spree::SingleStoreResource
26
+
27
+ belongs_to :store, class_name: 'Spree::Store'
28
+ belongs_to :channel, class_name: 'Spree::Channel'
29
+
30
+ attribute :active, :boolean, default: true
31
+
32
+ validates :type, :channel, presence: true
33
+ validates :position, presence: true, numericality: { only_integer: true }
34
+ validate :channel_belongs_to_store
35
+
36
+ scope :active, -> { where(active: true) }
37
+ scope :ordered, -> { order(:position) }
38
+ scope :for_channel, ->(channel) { where(channel_id: channel.id) }
39
+
40
+ acts_as_list scope: :channel_id
41
+
42
+ self.whitelisted_ransackable_attributes = %w[type position active store_id channel_id]
43
+
44
+ validate :type_must_be_registered
45
+
46
+ # Subclasses override. Returns an Array<LocationRanking> — one per location,
47
+ # with rank=nil to abstain.
48
+ #
49
+ # @param order [Spree::Order]
50
+ # @param locations [Array<Spree::StockLocation>]
51
+ # @return [Array<LocationRanking>]
52
+ def rank(_order, _locations)
53
+ raise NotImplementedError, "#{self.class} must implement #rank(order, locations)"
54
+ end
55
+
56
+ private
57
+
58
+ # The +type+ presence validation already covers blank; here we only reject
59
+ # a present-but-unregistered STI type so arbitrary class names can't be
60
+ # persisted via the +type+ column.
61
+ def type_must_be_registered
62
+ return if type.blank?
63
+ return if Spree.order_routing.rules.any? { |rule| rule.to_s == type }
64
+
65
+ errors.add(:type, Spree.t(:invalid_order_routing_rule, scope: [:errors, :messages], default: 'is not a registered order routing rule'))
66
+ end
67
+
68
+ def channel_belongs_to_store
69
+ return if channel.nil? || store_id.nil?
70
+ return if channel.store_id == store_id
71
+
72
+ errors.add(:channel, Spree.t('errors.messages.channel_store_mismatch'))
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module Spree
2
+ class PaymentSetupSessions::Bogus < PaymentSetupSession
3
+ end
4
+ end
@@ -23,15 +23,31 @@ module Spree
23
23
  can :manage, Spree::Zone
24
24
  can :manage, Spree::ZoneMember
25
25
 
26
+ # Markets — Channel / Market is the long-term replacement for
27
+ # Zone (see docs/plans/6.0-tax-provider.md), but both coexist
28
+ # during the migration and need admin read/write either way.
29
+ can :manage, Spree::Market
30
+
26
31
  # Tax configuration
27
32
  can :manage, Spree::TaxCategory
28
33
  can :manage, Spree::TaxRate
29
34
 
35
+ # CORS allowlist used by Rack::Cors + admin cookie auth (see
36
+ # docs/plans/5.5-admin-auth-cookie-refresh.md).
37
+ can :manage, Spree::AllowedOrigin
38
+
39
+ # Webhooks
40
+ can :manage, Spree::WebhookEndpoint
41
+ can :manage, Spree::WebhookDelivery
42
+
30
43
  # General configuration
31
44
  can :manage, Spree::RefundReason
32
45
  can :manage, Spree::ReimbursementType
33
46
  can :manage, Spree::ReturnReason
34
47
 
48
+ # Channels
49
+ can :manage, Spree::Channel
50
+
35
51
  # Restrictions on immutable types
36
52
  cannot [:edit, :update], Spree::RefundReason, mutable: false
37
53
  cannot [:edit, :update], Spree::ReimbursementType, mutable: false
@@ -19,6 +19,8 @@ module Spree
19
19
  can [:read, :admin], Spree::Taxonomy
20
20
  can [:read, :admin], Spree::Classification
21
21
  can [:read, :admin], Spree::Price
22
+ can [:read, :admin], Spree::PriceList
23
+ can [:read, :admin], Spree::PriceRule
22
24
  end
23
25
  end
24
26
  end
@@ -18,6 +18,8 @@ module Spree
18
18
  can :manage, Spree::Taxonomy
19
19
  can :manage, Spree::Classification
20
20
  can :manage, Spree::Price
21
+ can :manage, Spree::PriceList
22
+ can :manage, Spree::PriceRule
21
23
  can :manage, Spree::Asset
22
24
  end
23
25
  end
@@ -55,7 +55,20 @@ module Spree
55
55
  money_methods :amount, :price, :compare_at_amount
56
56
  alias display_compare_at_price display_compare_at_amount
57
57
 
58
- self.whitelisted_ransackable_attributes = ['amount', 'compare_at_amount']
58
+ self.whitelisted_ransackable_attributes = %w[amount compare_at_amount currency price_list_id variant_id]
59
+ self.whitelisted_ransackable_associations = %w[variant price_list]
60
+ self.whitelisted_ransackable_scopes = %i[search]
61
+
62
+ # Free-text search delegated to `Spree::Variant.search` (SKU + product
63
+ # name + option-value presentation), wrapped in a subquery so that
64
+ # multi-option-value variants don't produce duplicate Price rows —
65
+ # the prices index has `collection_distinct?` off (PG DISTINCT +
66
+ # ORDER BY incompat), so any join-based predicate would double up.
67
+ scope :search, ->(query) {
68
+ next all if query.blank?
69
+
70
+ where(variant_id: Spree::Variant.search(query).select(:id))
71
+ }
59
72
 
60
73
  attribute :eligible_for_taxon_matching, :boolean, default: false
61
74
  before_validation -> { self.eligible_for_taxon_matching = new_record? ? discounted? : discounted? != was_discounted? }