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,72 @@
1
+ require 'forwardable'
2
+
3
+ module Spree
4
+ module Authentication
5
+ # Keyed registry of authentication strategy classes for the Store and Admin APIs.
6
+ #
7
+ # Strategies are dispatched by the `provider` value the client sends to the auth
8
+ # endpoint, so the registry is a key → class map. The `:email` key is reserved for
9
+ # the built-in {Spree::Authentication::Strategies::EmailPasswordStrategy}; integrators
10
+ # can override it by adding a different class under the same key.
11
+ #
12
+ # @example Registering a custom provider
13
+ # Spree.store_authentication_strategies.add(:auth0, MyApp::Auth::Auth0Strategy)
14
+ #
15
+ # @example Removing a provider
16
+ # Spree.store_authentication_strategies.remove(:email)
17
+ #
18
+ # @example Reading a strategy class
19
+ # Spree.store_authentication_strategies[:email]
20
+ class StrategyRegistry
21
+ extend Forwardable
22
+ include Enumerable
23
+
24
+ def_delegators :@strategies, :keys, :values, :each
25
+
26
+ def initialize(strategies = {})
27
+ @strategies = {}
28
+ strategies.each { |key, klass| add(key, klass) }
29
+ end
30
+
31
+ # Register a strategy class under the given provider key. Overwrites any
32
+ # existing entry for that key.
33
+ #
34
+ # @param key [Symbol, String] provider identifier sent by the client
35
+ # @param strategy_class [Class] strategy class (typically a subclass of
36
+ # {Spree::Authentication::Strategies::BaseStrategy})
37
+ # @return [Class] the registered class
38
+ def add(key, strategy_class)
39
+ @strategies[key.to_sym] = strategy_class
40
+ end
41
+
42
+ # Unregister a strategy. Idempotent — returns `nil` if the key is not present.
43
+ #
44
+ # @param key [Symbol, String]
45
+ # @return [Class, nil] the removed class, or nil if no such key
46
+ def remove(key)
47
+ @strategies.delete(key.to_sym)
48
+ end
49
+
50
+ # Look up a registered strategy class.
51
+ #
52
+ # @param key [Symbol, String] provider identifier
53
+ # @return [Class, nil] the registered strategy class, or nil if no such key
54
+ def [](key)
55
+ @strategies[key.to_sym]
56
+ end
57
+
58
+ # Whether a strategy is registered under the given provider key.
59
+ #
60
+ # @param key [Symbol, String]
61
+ # @return [Boolean]
62
+ def key?(key)
63
+ @strategies.key?(key.to_sym)
64
+ end
65
+
66
+ # @return [Hash{Symbol => Class}] a shallow copy of the underlying map
67
+ def to_h
68
+ @strategies.dup
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,10 +1,12 @@
1
1
  class Spree::Base < ApplicationRecord
2
2
  include Spree::Preferences::Preferable
3
+ include Spree::PreferenceSchema
3
4
  include Spree::RansackableAttributes
4
5
  include Spree::TranslatableResourceScopes
5
6
  include Spree::IntegrationsConcern
6
7
  include Spree::Publishable
7
8
  include Spree::PrefixedId
9
+ include Spree::TypedAssociations
8
10
 
9
11
  after_initialize do
10
12
  if has_attribute?(:preferences) && !preferences.nil?
@@ -57,6 +59,10 @@ class Spree::Base < ApplicationRecord
57
59
  true
58
60
  end
59
61
 
62
+ def mysql_adapter?
63
+ ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
64
+ end
65
+
60
66
  def self.json_api_columns
61
67
  column_names.reject { |c| c.match(/_id$|id|preferences|(.*)password|(.*)token|(.*)api_key|^original_(.*)/) }
62
68
  end
@@ -71,10 +77,21 @@ class Spree::Base < ApplicationRecord
71
77
  column_names.reject { |c| skipped_attributes.include?(c.to_s) }
72
78
  end
73
79
 
74
- def self.json_api_type
80
+ # Public-API shorthand for this class, used as the `type` value on the
81
+ # wire (e.g. `"currency"` for `Spree::Promotion::Rules::Currency`,
82
+ # `"flat_rate"` for `Spree::Calculator::FlatRate`). Defaults to the
83
+ # demodulized + underscored leaf; override on a subclass when the wire
84
+ # format should stay stable across class renames.
85
+ def self.api_type
75
86
  to_s.demodulize.underscore
76
87
  end
77
88
 
89
+ # Backwards-compatible alias for `.api_type`. Delegates so subclass
90
+ # overrides of `api_type` are honored.
91
+ def self.json_api_type
92
+ api_type
93
+ end
94
+
78
95
  def self.to_tom_select_json
79
96
  pluck(:name, :id).map do |name, id|
80
97
  {
@@ -0,0 +1,159 @@
1
+ module Spree
2
+ # Lightweight distribution surface within a Store: online storefront, POS,
3
+ # marketplace integration, wholesale portal. Channels carry order
4
+ # attribution and the routing-strategy override.
5
+ class Channel < Spree.base_class
6
+ DEFAULT_CODE = 'online'.freeze
7
+
8
+ has_prefix_id :ch
9
+
10
+ include Spree::SingleStoreResource
11
+ include Spree::Metafields
12
+ include Spree::Metadata
13
+ include Spree::OrderRouting::HasStrategyPreference
14
+
15
+ # Empty -> falls back to the Store-level preference.
16
+ preference :order_routing_strategy, :string, default: nil
17
+
18
+ belongs_to :store, class_name: 'Spree::Store'
19
+
20
+ has_many :orders, class_name: 'Spree::Order', inverse_of: :channel, dependent: :nullify
21
+ has_many :order_routing_rules, class_name: 'Spree::OrderRoutingRule', dependent: :destroy
22
+ has_many :publications, class_name: 'Spree::ProductPublication', dependent: :destroy
23
+ has_many :products, through: :publications, class_name: 'Spree::Product'
24
+
25
+ attribute :active, :boolean, default: true
26
+
27
+ # HTTP headers arrive as ASCII-8BIT; +parameterize+ raises on those.
28
+ normalizes :code, with: ->(value) { value.to_s.dup.force_encoding(Encoding::UTF_8).parameterize.presence }
29
+
30
+ before_validation :backfill_code_from_name, if: -> { code.blank? && name.present? }
31
+ before_validation :promote_first_channel_to_default
32
+
33
+ validates :name, :store, presence: true
34
+ validates :code, presence: true, uniqueness: { scope: spree_base_uniqueness_scope + [:store_id] }
35
+
36
+ # Demote any prior default in the same transaction so the partial unique
37
+ # index ("only one default per store") never sees two TRUE rows. Runs
38
+ # before save so MySQL — which can't enforce a partial unique index — also
39
+ # arrives at a single default without relying on DB constraints.
40
+ before_save :demote_other_defaults, if: -> { default? && will_save_change_to_default? }
41
+ before_destroy :ensure_not_default
42
+ after_create :ensure_default_order_routing_rules
43
+
44
+ scope :active, -> { where(active: true) }
45
+ scope :default, -> { where(default: true) }
46
+
47
+ self.whitelisted_ransackable_attributes = %w[name code active default store_id]
48
+
49
+ # Publishes the given products on this channel by creating/upserting ProductPublications.
50
+ # Optionally sets the publication window; if not given, the products will be published immediately
51
+ # with no end date.
52
+ # @param product_ids [Array<Integer>, Integer] the IDs of the products to publish on this channel
53
+ # @param published_at [Time, nil] when the publications go live; nil means immediately
54
+ # @param unpublished_at [Time, nil] when the publications come down; nil means never
55
+ # @return [Integer] the number of ProductPublications created or updated
56
+ def add_products(product_ids, published_at: nil, unpublished_at: nil)
57
+ product_ids = Array(product_ids).map(&:to_s).uniq
58
+ return 0 if product_ids.empty?
59
+
60
+ now = Time.current
61
+ # Only include window columns in the upsert payload when the caller
62
+ # explicitly passed a value. Leaving them out keeps existing
63
+ # publication schedules intact on re-publish — otherwise +on_duplicate:
64
+ # :update+ + +update_only+ would rewrite scheduled +published_at+ /
65
+ # +unpublished_at+ to NULL whenever the bulk action re-runs without
66
+ # dates.
67
+ base = { channel_id: id, created_at: now, updated_at: now }
68
+ base[:published_at] = published_at unless published_at.nil?
69
+ base[:unpublished_at] = unpublished_at unless unpublished_at.nil?
70
+
71
+ records_to_upsert = product_ids.map { |product_id| base.merge(product_id: product_id) }
72
+
73
+ # Only update the window columns the caller passed. When neither was
74
+ # passed, treat re-publish as a no-op (+on_duplicate: :skip+ → MySQL
75
+ # +INSERT IGNORE+, PG/SQLite +ON CONFLICT DO NOTHING+).
76
+ update_columns = []
77
+ update_columns << :published_at unless published_at.nil?
78
+ update_columns << :unpublished_at unless unpublished_at.nil?
79
+ opts = if update_columns.empty?
80
+ { on_duplicate: :skip }
81
+ else
82
+ { record_timestamps: false, update_only: update_columns, on_duplicate: :update }
83
+ end
84
+ # MySQL infers the conflict target from the table's unique constraints
85
+ # and rejects an explicit +unique_by+; PostgreSQL/SQLite require it.
86
+ opts[:unique_by] = %i[product_id channel_id] unless mysql_adapter?
87
+
88
+ Spree::ProductPublication.upsert_all(records_to_upsert, **opts)
89
+
90
+ products = Spree::Product.where(id: product_ids)
91
+ products.touch_all
92
+ products.each(&:enqueue_search_index)
93
+ touch
94
+
95
+ records_to_upsert.size
96
+ end
97
+
98
+ # Unpublishes the given products from this channel.
99
+ # @param product_ids [Array<Integer>, Integer] the IDs of the products to unpublish
100
+ # @return [Integer] the number of ProductPublications destroyed
101
+ def remove_products(product_ids)
102
+ product_ids = Array(product_ids).map(&:to_s).uniq
103
+ return 0 if product_ids.empty?
104
+
105
+ count = publications.where(product_id: product_ids).destroy_all.size
106
+
107
+ products = Spree::Product.where(id: product_ids)
108
+ products.touch_all
109
+ products.each(&:enqueue_search_index)
110
+
111
+ touch if count.positive?
112
+ count
113
+ end
114
+
115
+ # @return [Boolean]
116
+ def can_be_deleted?
117
+ !default?
118
+ end
119
+
120
+ private
121
+
122
+ def ensure_not_default
123
+ return if can_be_deleted?
124
+ return if destroyed_by_association.present?
125
+
126
+ errors.add(:base, Spree.t('errors.messages.cannot_delete_default_channel'))
127
+ throw :abort
128
+ end
129
+
130
+ def backfill_code_from_name
131
+ self.code = name
132
+ end
133
+
134
+ # First channel on a store becomes the default. Lets the
135
+ # +Stores::Channels#ensure_default_channel+ seed path and the legacy
136
+ # admin "create channel" form both produce a sensible default without
137
+ # the caller having to know.
138
+ def promote_first_channel_to_default
139
+ return if default
140
+ return unless new_record? && store_id.present?
141
+
142
+ self.default = true unless Spree::Channel.where(store_id: store_id).exists?
143
+ end
144
+
145
+ def demote_other_defaults
146
+ Spree::Channel.where(store_id: store_id, default: true).where.not(id: id).update_all(default: false)
147
+ end
148
+
149
+ # Default ordering: preferred location wins, then minimize splits, then
150
+ # fall back to StockLocation.default. See docs/plans/6.0-order-routing.md.
151
+ def ensure_default_order_routing_rules
152
+ return if order_routing_rules.any?
153
+
154
+ Spree::OrderRouting::Rules::PreferredLocation.create!(store: store, channel: self, position: 1)
155
+ Spree::OrderRouting::Rules::MinimizeSplits.create!(store: store , channel: self, position: 2)
156
+ Spree::OrderRouting::Rules::DefaultLocation.create!(store: store, channel: self, position: 3)
157
+ end
158
+ end
159
+ end
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Country < Spree.base_class
3
+ self.whitelisted_ransackable_attributes = %w[iso iso3 iso_name name]
4
+
3
5
  has_many :addresses, dependent: :restrict_with_error
4
6
  has_many :states,
5
7
  -> { order name: :asc },
@@ -4,7 +4,7 @@ module Spree
4
4
  # All attributes are automatically reset between requests by Rails.
5
5
  # Fallback chains ensure sensible defaults when attributes are not explicitly set.
6
6
  class Current < ::ActiveSupport::CurrentAttributes
7
- attribute :store, :market, :currency, :locale, :zone, :default_tax_zone, :price_lists, :global_pricing_context
7
+ attribute :store, :channel, :market, :currency, :locale, :zone, :default_tax_zone, :price_lists, :global_pricing_context
8
8
 
9
9
  resets { @default_tax_zone_loaded = false }
10
10
 
@@ -14,6 +14,10 @@ module Spree
14
14
  super || Spree::Store.default
15
15
  end
16
16
 
17
+ def channel
18
+ super || (self.channel = store&.default_channel)
19
+ end
20
+
17
21
  # Returns the current market, falling back to the store's default market.
18
22
  # @return [Spree::Market, nil]
19
23
  def market
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ # Constant alias for the legacy Spree::Metafield class. Lets controllers,
3
+ # serializers, and 5.5+ extensions reference the model by its 6.0-bound name
4
+ # without the actual class rename (which lands with the table rename in 6.0).
5
+ #
6
+ # This is a true constant alias — the underlying class, table, STI subclasses,
7
+ # and `model_name` are all `Spree::Metafield`. Only the constant name differs.
8
+ CustomField = Metafield
9
+ end
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ # Constant alias for the legacy Spree::MetafieldDefinition class. Lets
3
+ # controllers, serializers, and 5.5+ extensions reference the model by its
4
+ # 6.0-bound name without the actual class rename (which lands with the table
5
+ # rename in 6.0).
6
+ CustomFieldDefinition = MetafieldDefinition
7
+ end
@@ -10,6 +10,11 @@ module Spree
10
10
  belongs_to :store, class_name: 'Spree::Store', inverse_of: :customer_groups
11
11
  has_many :customer_group_users, class_name: 'Spree::CustomerGroupUser', dependent: :destroy
12
12
  has_many :users, through: :customer_group_users, source: :user, source_type: Spree.user_class.to_s
13
+ # `customers` is the public name across the v3 API; declaring it as its
14
+ # own association (rather than `alias_method`) is what lets `customer_ids=`
15
+ # exist and what makes the `PrefixedId` auto-decoder in `assign_attributes`
16
+ # recognise the key — that lookup keys off `reflect_on_association(:customers)`.
17
+ has_many :customers, through: :customer_group_users, source: :user, source_type: Spree.user_class.to_s
13
18
 
14
19
  #
15
20
  # Validations
@@ -25,9 +30,10 @@ module Spree
25
30
  #
26
31
  # Instance Methods
27
32
  #
28
- def users_count
29
- customer_group_users.count
33
+ def customers_count
34
+ customer_group_users.size
30
35
  end
36
+ alias_method :users_count, :customers_count
31
37
 
32
38
  # Bulk add customers to the group
33
39
  # @param user_ids [Array] array of user IDs to add
@@ -6,20 +6,20 @@ module Spree
6
6
  # Events are immutable objects that carry information about something
7
7
  # that happened in the system. They contain:
8
8
  # - An id (UUID)
9
- # - A name (e.g., 'order.complete', 'product.create')
9
+ # - A name (e.g., 'order.completed', 'product.created')
10
10
  # - A store_id (the store where the event originated)
11
11
  # - A payload (serialized data about the event)
12
12
  # - Metadata (contextual information like spree_version)
13
13
  #
14
14
  # @example Creating an event
15
15
  # event = Spree::Event.new(
16
- # name: 'order.complete',
16
+ # name: 'order.completed',
17
17
  # payload: order.serializable_hash
18
18
  # )
19
19
  #
20
20
  # @example Accessing event data
21
21
  # event.id # => "550e8400-e29b-41d4-a716-446655440000"
22
- # event.name # => 'order.complete'
22
+ # event.name # => 'order.completed'
23
23
  # event.store_id # => 1
24
24
  # event.payload # => { 'id' => 1, 'number' => 'R123456' }
25
25
  # event.created_at # => 2024-01-15 10:30:00 UTC
@@ -61,19 +61,19 @@ module Spree
61
61
  end
62
62
 
63
63
  # Returns the resource type from the event name
64
- # @return [String] The resource type (e.g., 'order' from 'order.complete')
64
+ # @return [String] The resource type (e.g., 'order' from 'order.completed')
65
65
  def resource_type
66
66
  @resource_type ||= name.to_s.split('.').first
67
67
  end
68
68
 
69
69
  # Returns the action from the event name
70
- # @return [String] The action (e.g., 'complete' from 'order.complete')
70
+ # @return [String] The action (e.g., 'completed' from 'order.completed')
71
71
  def action
72
72
  @action ||= name.to_s.split('.').drop(1).join('.')
73
73
  end
74
74
 
75
75
  # Checks if the event matches a pattern
76
- # Supports wildcards: 'order.*' matches 'order.complete', 'order.cancel'
76
+ # Supports wildcards: 'order.*' matches 'order.completed', 'order.canceled'
77
77
  # @param pattern [String] The pattern to match against
78
78
  # @return [Boolean]
79
79
  def matches?(pattern)
@@ -22,13 +22,16 @@ module Spree
22
22
  # Associations
23
23
  #
24
24
  belongs_to :store, class_name: 'Spree::Store'
25
- belongs_to :user, class_name: Spree.admin_user_class.to_s
25
+ # Optional so secret-API-key callers (apps / server-to-server) can create
26
+ # exports without a human user attached. The email notification is
27
+ # skipped for these — apps poll instead.
28
+ belongs_to :user, class_name: Spree.admin_user_class.to_s, optional: true
26
29
  belongs_to :vendor, -> { with_deleted }, class_name: 'Spree::Vendor', optional: true
27
30
 
28
31
  #
29
32
  # Validations
30
33
  #
31
- validates :format, :store, :user, :type, presence: true
34
+ validates :format, :store, :type, presence: true
32
35
 
33
36
  #
34
37
  # Enums
@@ -83,10 +86,10 @@ module Spree
83
86
  batch.each do |record|
84
87
  if multi_line_csv?
85
88
  record.to_csv(store).each do |line|
86
- csv << line
89
+ csv << Spree::CSV::FormulaSanitizer.row(line)
87
90
  end
88
91
  else
89
- csv << record.to_csv(store)
92
+ csv << Spree::CSV::FormulaSanitizer.row(record.to_csv(store))
90
93
  end
91
94
  end
92
95
  end
@@ -127,12 +130,34 @@ module Spree
127
130
 
128
131
  def records_to_export
129
132
  if search_params.present?
130
- scope.ransack(search_params.is_a?(String) ? JSON.parse(search_params.to_s).to_h : search_params)
133
+ params = search_params.is_a?(String) ? JSON.parse(search_params.to_s).to_h : search_params
134
+ scope.ransack(decode_prefixed_id_filters(params))
131
135
  else
132
136
  scope.ransack
133
137
  end.result
134
138
  end
135
139
 
140
+ # Replace any prefixed IDs in `search_params` with their raw DB IDs so
141
+ # Ransack can match them. Without this, an admin filtering an export by
142
+ # a foreign key (`promotion_id_eq: 'promo_xxx'`, `vendor_id_in: [...]`)
143
+ # would always get zero rows. We only touch values that look like
144
+ # prefixed IDs — anything else (numeric IDs, code strings, ranges,
145
+ # state names) passes through untouched.
146
+ def decode_prefixed_id_filters(params)
147
+ params.transform_values { |value| decode_search_value(value) }
148
+ end
149
+
150
+ def decode_search_value(value)
151
+ case value
152
+ when String
153
+ Spree::PrefixedId.prefixed_id?(value) ? (Spree::PrefixedId.decode_prefixed_id(value) || value) : value
154
+ when Array
155
+ value.map { |v| decode_search_value(v) }
156
+ else
157
+ value
158
+ end
159
+ end
160
+
136
161
  def scope_includes
137
162
  []
138
163
  end
@@ -181,6 +206,8 @@ module Spree
181
206
  end
182
207
 
183
208
  def send_export_done_email
209
+ return if user.blank? # App-created exports (secret API key) have no user to email.
210
+
184
211
  Spree::ExportMailer.export_done(self).deliver_later
185
212
  end
186
213
 
@@ -30,7 +30,7 @@ module Spree
30
30
  records_to_export.includes(scope_includes).find_in_batches do |batch|
31
31
  batch.each do |product|
32
32
  product.to_translation_csv(store, locales).each do |line|
33
- csv << line
33
+ csv << Spree::CSV::FormulaSanitizer.row(line)
34
34
  end
35
35
  end
36
36
  end
@@ -120,9 +120,14 @@ module Spree
120
120
  true
121
121
  end
122
122
 
123
+ def payment_setup_session_class
124
+ PaymentSetupSessions::Bogus
125
+ end
126
+
123
127
  def create_payment_setup_session(customer:, external_data: {})
124
- payment_setup_sessions.create(
128
+ payment_setup_session_class.create(
125
129
  customer: customer,
130
+ payment_method: self,
126
131
  status: 'pending',
127
132
  external_id: "bogus_seti_#{SecureRandom.hex(12)}",
128
133
  external_client_secret: "bogus_seti_secret_#{SecureRandom.hex(8)}",
@@ -6,6 +6,31 @@ module Spree
6
6
 
7
7
  validates :type, presence: true, inclusion: { in: :valid_providers_list }
8
8
 
9
+ # Payment provider gems conventionally ship a top-level `Gateway`
10
+ # class — `SpreeStripe::Gateway`, `SpreeAdyen::Gateway`,
11
+ # `SpreePaypalCheckout::Gateway`. The default demodulized
12
+ # `api_type` collapses every provider to `"gateway"`, which
13
+ # collides in the registry and produces duplicate keys in admin
14
+ # UIs. For gateway subclasses, use the outer module instead (with
15
+ # a leading `Spree` namespace stripped), matching the labelling
16
+ # convention in `PreferenceSchema#subclass_label`:
17
+ #
18
+ # SpreeStripe::Gateway → "stripe"
19
+ # SpreeAdyen::Gateway → "adyen"
20
+ # SpreePaypalCheckout::Gateway → "paypal_checkout"
21
+ # MyShop::Gateway → "my_shop"
22
+ #
23
+ # Subclasses nested under a Gateway module (e.g.
24
+ # `Spree::Gateway::Bogus`) demodulize to their own leaf and
25
+ # bypass this fallback.
26
+ def self.api_type
27
+ leaf = super
28
+ return leaf unless leaf == 'gateway'
29
+
30
+ outer = to_s.deconstantize.delete_prefix('Spree::').delete_prefix('Spree').presence
31
+ outer ? outer.underscore : leaf
32
+ end
33
+
9
34
  def payment_source_class
10
35
  CreditCard
11
36
  end
@@ -62,7 +62,7 @@ module Spree
62
62
  #
63
63
  # Ransack
64
64
  #
65
- self.whitelisted_ransackable_attributes = %w[code user_id state]
65
+ self.whitelisted_ransackable_attributes = %w[code user_id state gift_card_batch_id created_by_id]
66
66
  self.whitelisted_ransackable_associations = %w[users orders batch]
67
67
  self.whitelisted_ransackable_scopes = %w[active expired redeemed partially_redeemed]
68
68
 
@@ -18,7 +18,10 @@ module Spree
18
18
  # Validations
19
19
  #
20
20
  validates :codes_count, :amount, :prefix, presence: true
21
- validates :codes_count, numericality: { greater_than: 0, less_than_or_equal_to: Spree::Config[:gift_card_batch_limit].to_i }
21
+ validates :codes_count, numericality: {
22
+ greater_than: 0,
23
+ less_than_or_equal_to: ->(_record) { Spree::Config[:gift_card_batch_limit].to_i }
24
+ }
22
25
  validates :store, :currency, presence: true
23
26
  validates :amount, numericality: { greater_than: 0 }
24
27
 
@@ -62,6 +62,7 @@ module Spree
62
62
  event :complete do
63
63
  transition from: :processing, to: :completed
64
64
  end
65
+ after_transition to: :completed, do: :touch_store
65
66
  after_transition to: :completed, do: :publish_import_completed_event
66
67
  # NOTE: send_import_completed_email and update_loader_in_import_view
67
68
  # are now handled by Spree::Admin::ImportSubscriber listening to 'import.completed' event
@@ -181,6 +182,10 @@ module Spree
181
182
  "#{Spree.t(type.demodulize.pluralize.downcase)} #{number}"
182
183
  end
183
184
 
185
+ def touch_store
186
+ store.touch
187
+ end
188
+
184
189
  def publish_import_completed_event
185
190
  publish_event('import.completed')
186
191
  end
@@ -41,6 +41,11 @@ module Spree
41
41
  # are now handled by Spree::Admin::ImportRowSubscriber
42
42
  end
43
43
 
44
+ # How long a row may sit in `processing` before we consider its owning worker dead
45
+ # (OOM, SIGKILL, deploy without graceful drain — none of these trigger a Sidekiq
46
+ # retry). After this window, stalled rows no longer block the import from completing.
47
+ STALLED_PROCESSING_AFTER = 1.hour
48
+
44
49
  #
45
50
  # Scopes
46
51
  #
@@ -48,6 +53,13 @@ module Spree
48
53
  scope :completed, -> { where(status: :completed) }
49
54
  scope :failed, -> { where(status: :failed) }
50
55
  scope :processed, -> { where(status: %i[completed failed]) }
56
+ # Rows still legitimately blocking import completion: `pending` (not started) or
57
+ # `processing` with a recent updated_at (worker still alive). Orphaned `processing`
58
+ # rows past the stall window are excluded so a dead worker can't permanently block
59
+ # completion — operators can clean those up separately.
60
+ scope :in_flight, -> {
61
+ where(status: :pending).or(where(status: :processing).where(updated_at: STALLED_PROCESSING_AFTER.ago..))
62
+ }
51
63
 
52
64
  def data_json
53
65
  @data_json ||= JSON.parse(data)
@@ -24,6 +24,7 @@ module Spree
24
24
  has_many :inventory_units, class_name: 'Spree::InventoryUnit', inverse_of: :line_item, dependent: :destroy
25
25
  has_many :shipments, through: :inventory_units, source: :shipment
26
26
  has_many :digital_links, dependent: :destroy
27
+ has_many :stock_reservations, class_name: 'Spree::StockReservation', inverse_of: :line_item, dependent: :destroy
27
28
 
28
29
  before_validation :copy_price
29
30
  before_validation :copy_tax_category
@@ -164,9 +165,13 @@ module Spree
164
165
 
165
166
  # Returns true if the line item has sufficient stock
166
167
  #
168
+ # The order's own active stock reservations are excluded from the
169
+ # availability check — a customer's own checkout hold must not make
170
+ # their own line item look out of stock.
171
+ #
167
172
  # @return [Boolean]
168
173
  def sufficient_stock?
169
- can_supply? quantity
174
+ Spree::Stock::Quantifier.new(variant, excluded_order: order).can_supply?(quantity)
170
175
  end
171
176
 
172
177
  # Returns true if the line item has insufficient stock
@@ -293,7 +298,7 @@ module Spree
293
298
  end
294
299
 
295
300
  def verify_order_inventory_before_destroy
296
- Spree::OrderInventory.new(order, self).verify(target_shipment)
301
+ Spree::OrderInventory.new(order, self).verify(target_shipment, removing: true)
297
302
  end
298
303
 
299
304
  def update_adjustments