spree_core 5.4.3 → 5.5.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) 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 +2 -1
  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/ransackable_attributes.rb +5 -1
  28. data/app/models/concerns/spree/search_indexable.rb +8 -7
  29. data/app/models/concerns/spree/searchable.rb +11 -2
  30. data/app/models/concerns/spree/stores/channels.rb +20 -0
  31. data/app/models/concerns/spree/stores/markets.rb +21 -5
  32. data/app/models/concerns/spree/typed_associations.rb +120 -0
  33. data/app/models/concerns/spree/user_methods.rb +71 -12
  34. data/app/models/spree/ability.rb +4 -117
  35. data/app/models/spree/api_key.rb +60 -0
  36. data/app/models/spree/asset.rb +28 -5
  37. data/app/models/spree/authentication/strategy_registry.rb +72 -0
  38. data/app/models/spree/base.rb +18 -1
  39. data/app/models/spree/channel.rb +159 -0
  40. data/app/models/spree/country.rb +2 -0
  41. data/app/models/spree/current.rb +7 -3
  42. data/app/models/spree/custom_field.rb +9 -0
  43. data/app/models/spree/custom_field_definition.rb +7 -0
  44. data/app/models/spree/customer_group.rb +8 -2
  45. data/app/models/spree/export.rb +45 -3
  46. data/app/models/spree/exports/coupon_codes.rb +4 -0
  47. data/app/models/spree/exports/newsletter_subscribers.rb +4 -0
  48. data/app/models/spree/exports/product_translations.rb +4 -0
  49. data/app/models/spree/gateway.rb +25 -0
  50. data/app/models/spree/gift_card.rb +1 -1
  51. data/app/models/spree/gift_card_batch.rb +4 -1
  52. data/app/models/spree/import.rb +5 -0
  53. data/app/models/spree/import_row.rb +12 -0
  54. data/app/models/spree/line_item.rb +6 -1
  55. data/app/models/spree/market.rb +32 -1
  56. data/app/models/spree/metafield.rb +38 -0
  57. data/app/models/spree/metafield_definition.rb +29 -6
  58. data/app/models/spree/metafields/json.rb +10 -0
  59. data/app/models/spree/newsletter_subscriber.rb +19 -3
  60. data/app/models/spree/option_type.rb +48 -7
  61. data/app/models/spree/order/checkout.rb +3 -3
  62. data/app/models/spree/order.rb +102 -6
  63. data/app/models/spree/order_approval.rb +19 -0
  64. data/app/models/spree/order_cancellation.rb +19 -0
  65. data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
  66. data/app/models/spree/order_routing/rules/default_location.rb +16 -0
  67. data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
  68. data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
  69. data/app/models/spree/order_routing/strategy/base.rb +47 -0
  70. data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
  71. data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
  72. data/app/models/spree/order_routing/strategy/rules.rb +83 -0
  73. data/app/models/spree/order_routing_rule.rb +75 -0
  74. data/app/models/spree/permission_sets/configuration_management.rb +16 -0
  75. data/app/models/spree/permission_sets/product_display.rb +2 -0
  76. data/app/models/spree/permission_sets/product_management.rb +2 -0
  77. data/app/models/spree/price.rb +14 -1
  78. data/app/models/spree/price_list.rb +129 -17
  79. data/app/models/spree/price_rule.rb +11 -1
  80. data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
  81. data/app/models/spree/price_rules/market_rule.rb +16 -1
  82. data/app/models/spree/price_rules/user_rule.rb +21 -2
  83. data/app/models/spree/product/channels.rb +149 -0
  84. data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
  85. data/app/models/spree/product/slugs.rb +1 -1
  86. data/app/models/spree/product.rb +172 -31
  87. data/app/models/spree/product_publication.rb +43 -0
  88. data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
  89. data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
  90. data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
  91. data/app/models/spree/promotion/rules/country.rb +40 -18
  92. data/app/models/spree/promotion/rules/customer_group.rb +10 -1
  93. data/app/models/spree/promotion/rules/product.rb +4 -0
  94. data/app/models/spree/promotion/rules/taxon.rb +24 -1
  95. data/app/models/spree/promotion/rules/user.rb +21 -0
  96. data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
  97. data/app/models/spree/promotion.rb +22 -1
  98. data/app/models/spree/promotion_action.rb +17 -11
  99. data/app/models/spree/promotion_rule.rb +17 -18
  100. data/app/models/spree/search_provider/meilisearch.rb +12 -2
  101. data/app/models/spree/stock/availability_validator.rb +1 -1
  102. data/app/models/spree/stock/quantifier.rb +89 -9
  103. data/app/models/spree/stock_item.rb +36 -0
  104. data/app/models/spree/stock_location.rb +52 -0
  105. data/app/models/spree/stock_reservation.rb +38 -0
  106. data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
  107. data/app/models/spree/store.rb +18 -72
  108. data/app/models/spree/store_credit.rb +0 -8
  109. data/app/models/spree/store_product.rb +11 -23
  110. data/app/models/spree/taxon.rb +0 -5
  111. data/app/models/spree/user_identity.rb +1 -2
  112. data/app/models/spree/variant.rb +132 -18
  113. data/app/models/spree/variant_media.rb +46 -0
  114. data/app/models/spree/webhook_delivery.rb +1 -1
  115. data/app/models/spree/webhook_endpoint.rb +24 -0
  116. data/app/models/spree/wished_item.rb +0 -13
  117. data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
  118. data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
  119. data/app/presenters/spree/variant_presenter.rb +4 -3
  120. data/app/services/spree/addresses/update.rb +6 -8
  121. data/app/services/spree/cart/add_item.rb +10 -0
  122. data/app/services/spree/cart/empty.rb +2 -0
  123. data/app/services/spree/cart/remove_line_item.rb +10 -0
  124. data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
  125. data/app/services/spree/cart/set_quantity.rb +10 -0
  126. data/app/services/spree/carts/complete.rb +1 -0
  127. data/app/services/spree/carts/create.rb +1 -0
  128. data/app/services/spree/carts/update.rb +18 -2
  129. data/app/services/spree/carts/upsert_items.rb +6 -6
  130. data/app/services/spree/imports/row_processors/customer.rb +4 -1
  131. data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
  132. data/app/services/spree/newsletter/link_user.rb +53 -0
  133. data/app/services/spree/newsletter/subscribe.rb +31 -9
  134. data/app/services/spree/orders/approve.rb +27 -6
  135. data/app/services/spree/orders/build_shipments.rb +29 -0
  136. data/app/services/spree/orders/cancel.rb +34 -3
  137. data/app/services/spree/orders/complete.rb +53 -0
  138. data/app/services/spree/orders/create.rb +156 -0
  139. data/app/services/spree/orders/update.rb +51 -0
  140. data/app/services/spree/orders/upsert_items.rb +70 -0
  141. data/app/services/spree/prices/bulk_upsert.rb +201 -0
  142. data/app/services/spree/products/duplicator.rb +1 -1
  143. data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
  144. data/app/services/spree/sample_data/loader.rb +30 -0
  145. data/app/services/spree/stock_reservations/extend.rb +19 -0
  146. data/app/services/spree/stock_reservations/release.rb +12 -0
  147. data/app/services/spree/stock_reservations/reserve.rb +103 -0
  148. data/app/services/spree/taxons/remove_products.rb +7 -1
  149. data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
  150. data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
  151. data/config/locales/en.yml +28 -10
  152. data/config/routes.rb +9 -0
  153. data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
  154. data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
  155. data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
  156. data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
  157. data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
  158. data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
  159. data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
  160. data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
  161. data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
  162. data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
  163. data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
  164. data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
  165. data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
  166. data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
  167. data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
  168. data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
  169. data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
  170. data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
  171. data/db/migrate/20260612000001_change_spree_user_identities_info_to_jsonb.rb +13 -0
  172. data/db/sample_data/channels.rb +12 -0
  173. data/db/sample_data/orders.rb +1 -1
  174. data/db/sample_data/products.csv +212 -212
  175. data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
  176. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
  177. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
  178. data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
  179. data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
  180. data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
  181. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
  182. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
  183. data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +17 -0
  184. data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
  185. data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
  186. data/lib/generators/spree/model/model_generator.rb +73 -7
  187. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
  188. data/lib/generators/spree/model/templates/model.rb.tt +28 -2
  189. data/lib/generators/spree/subscriber/subscriber_generator.rb +116 -0
  190. data/lib/generators/spree/subscriber/templates/subscriber.rb.tt +17 -0
  191. data/lib/generators/spree/subscriber/templates/subscriber_spec.rb.tt +9 -0
  192. data/lib/spree/core/configuration.rb +7 -0
  193. data/lib/spree/core/controller_helpers/auth.rb +0 -12
  194. data/lib/spree/core/controller_helpers/currency.rb +0 -17
  195. data/lib/spree/core/controller_helpers/order.rb +0 -19
  196. data/lib/spree/core/dependencies.rb +5 -2
  197. data/lib/spree/core/engine.rb +54 -7
  198. data/lib/spree/core/permission_configuration.rb +15 -0
  199. data/lib/spree/core/preferences/masking.rb +47 -0
  200. data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
  201. data/lib/spree/core/version.rb +1 -1
  202. data/lib/spree/core.rb +56 -5
  203. data/lib/spree/permitted_attributes.rb +9 -7
  204. data/lib/spree/testing_support/factories/address_factory.rb +16 -9
  205. data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
  206. data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
  207. data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
  208. data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
  209. data/lib/spree/testing_support/factories/product_factory.rb +16 -7
  210. data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
  211. data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
  212. data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
  213. data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
  214. data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
  215. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  216. data/lib/spree/testing_support/store.rb +10 -0
  217. data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
  218. data/lib/tasks/channels.rake +94 -0
  219. data/lib/tasks/cli.rake +2 -1
  220. data/lib/tasks/core.rake +1 -0
  221. data/lib/tasks/media.rake +27 -0
  222. data/lib/tasks/products.rake +4 -6
  223. data/lib/tasks/publications.rake +60 -0
  224. data/lib/tasks/upgrade.rake +211 -0
  225. metadata +87 -18
  226. data/app/finders/spree/variants/visible_finder.rb +0 -23
  227. data/app/paginators/spree/shared/paginate.rb +0 -30
  228. data/app/presenters/spree/filters/price_presenter.rb +0 -23
  229. data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
  230. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
  231. data/app/presenters/spree/product_summary_presenter.rb +0 -27
  232. data/app/presenters/spree/variants/options_presenter.rb +0 -82
  233. data/app/services/spree/classifications/reposition.rb +0 -23
  234. data/app/sorters/spree/orders/sort.rb +0 -10
  235. data/lib/spree/core/controller_helpers/common.rb +0 -14
  236. data/lib/spree/core/token_generator.rb +0 -23
  237. data/lib/spree/database_type_utilities.rb +0 -22
  238. data/lib/spree/testing_support/bar_ability.rb +0 -14
  239. data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Preferences
5
+ # Masks `:password`-typed preferences so secrets (API keys, OAuth
6
+ # tokens, signing secrets, …) never leave the server in plaintext.
7
+ #
8
+ # The mask token is a bullet sequence followed by the last 4
9
+ # characters of the original value — Stripe's "stored, here's the
10
+ # last 4" pattern.
11
+ module Masking
12
+ TOKEN = '••••'
13
+
14
+ # @param value [Object] the preference value to mask
15
+ # @return [String, nil] masked string, or nil if value is blank
16
+ def self.mask(value)
17
+ return nil if value.blank?
18
+
19
+ "#{TOKEN}#{value.to_s.last(4)}"
20
+ end
21
+
22
+ # @param value [Object] a value previously returned by `mask`
23
+ # @return [Boolean] true if value carries the mask token
24
+ def self.masked?(value)
25
+ value.is_a?(String) && value.start_with?(TOKEN)
26
+ end
27
+
28
+ # Serializes a Preferable's `preferences` hash for the wire,
29
+ # masking `:password` values. Keys are stringified to match the
30
+ # wire shape expected by JSON clients — schema entries built by
31
+ # `compute_preference_schema` cache `:key_string` to avoid a
32
+ # `to_s` allocation per field per request.
33
+ #
34
+ # @param preferable [#preferences, #preference_schema, nil] any object
35
+ # that includes `Spree::Preferences::Preferable` and `Spree::PreferenceSchema`
36
+ # @return [Hash{String => Object}]
37
+ def self.serialize(preferable)
38
+ return {} if preferable.nil?
39
+
40
+ preferable.preference_schema.each_with_object({}) do |field, hash|
41
+ value = preferable.preferences[field[:key]]
42
+ hash[field[:key_string] || field[:key].to_s] = field[:type] == :password ? mask(value) : value
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -18,7 +18,13 @@ module Spree::Preferences
18
18
  end
19
19
 
20
20
  define_method preference_setter_method(name) do |value|
21
- value = parse_on_set.call(value) if parse_on_set.is_a?(Proc)
21
+ if parse_on_set.is_a?(Proc)
22
+ # Procs that accept more than one arg opt into receiving the
23
+ # owning record so they can scope (e.g. `normalize_id_preference`
24
+ # rejecting cross-store IDs). `arity.abs > 1` covers both the
25
+ # `(value, owner)` and `(value, owner = nil)` shapes.
26
+ value = parse_on_set.arity.abs > 1 ? parse_on_set.call(value, self) : parse_on_set.call(value)
27
+ end
22
28
  value = convert_preference_value(value, type, nullable: nullable)
23
29
  preferences[name] = value
24
30
 
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.4.3'.freeze
2
+ VERSION = '5.5.0.rc2'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/spree/core.rb CHANGED
@@ -112,7 +112,8 @@ module Spree
112
112
  webhooks: :default,
113
113
  payment_webhooks: :default,
114
114
  api_keys: :default,
115
- search: :default
115
+ search: :default,
116
+ stock_reservations: :default
116
117
  )
117
118
  end
118
119
 
@@ -241,6 +242,14 @@ module Spree
241
242
  Rails.application.config.spree.stock_splitters = value
242
243
  end
243
244
 
245
+ def self.order_routing
246
+ Rails.application.config.spree.order_routing
247
+ end
248
+
249
+ def self.order_routing=(value)
250
+ Rails.application.config.spree.order_routing = value
251
+ end
252
+
244
253
  def self.promotions
245
254
  Rails.application.config.spree.promotions
246
255
  end
@@ -289,6 +298,21 @@ module Spree
289
298
  Rails.application.config.spree.taxon_rules = value
290
299
  end
291
300
 
301
+ # Class-name strings (`'Spree::Product'`, `'Spree::Order'`,
302
+ # `Spree.user_class.to_s`, plus any registered by apps) for resources that
303
+ # expose tags via `acts_as_taggable_on :tags`. Used by the Admin API
304
+ # `/tags` autocomplete endpoint to validate `taggable_type`. Apps extend
305
+ # the list in an initializer:
306
+ #
307
+ # Spree.taggable_types << 'MyApp::Vendor'
308
+ def self.taggable_types
309
+ Rails.application.config.spree.taggable_types
310
+ end
311
+
312
+ def self.taggable_types=(value)
313
+ Rails.application.config.spree.taggable_types = value
314
+ end
315
+
292
316
  def self.reports
293
317
  Rails.application.config.spree.reports
294
318
  end
@@ -338,6 +362,36 @@ module Spree
338
362
  Rails.application.config.spree.pricing = value
339
363
  end
340
364
 
365
+ # Registry of authentication strategy classes for the Store API.
366
+ # @return [Spree::Authentication::StrategyRegistry]
367
+ # @example Registering a third-party identity provider
368
+ # Spree.store_authentication_strategies.add(:auth0, MyApp::Auth::Auth0Strategy)
369
+ # @example Removing a strategy
370
+ # Spree.store_authentication_strategies.remove(:email)
371
+ def self.store_authentication_strategies
372
+ Rails.application.config.spree.store_authentication_strategies
373
+ end
374
+
375
+ # @param value [Spree::Authentication::StrategyRegistry] the registry to use for Store API authentication dispatch
376
+ # @return [Spree::Authentication::StrategyRegistry] the assigned registry
377
+ def self.store_authentication_strategies=(value)
378
+ Rails.application.config.spree.store_authentication_strategies = value
379
+ end
380
+
381
+ # Registry of authentication strategy classes for the Admin API.
382
+ # @return [Spree::Authentication::StrategyRegistry]
383
+ # @example Registering an SSO strategy for admin users
384
+ # Spree.admin_authentication_strategies.add(:okta, MyApp::Auth::OktaStrategy)
385
+ def self.admin_authentication_strategies
386
+ Rails.application.config.spree.admin_authentication_strategies
387
+ end
388
+
389
+ # @param value [Spree::Authentication::StrategyRegistry] the registry to use for Admin API authentication dispatch
390
+ # @return [Spree::Authentication::StrategyRegistry] the assigned registry
391
+ def self.admin_authentication_strategies=(value)
392
+ Rails.application.config.spree.admin_authentication_strategies = value
393
+ end
394
+
341
395
  def self.analytics
342
396
  @analytics ||= AnalyticsConfig.new
343
397
  end
@@ -425,8 +479,6 @@ module Spree
425
479
  end
426
480
 
427
481
  module Core
428
- autoload :TokenGenerator, 'spree/core/token_generator'
429
-
430
482
  class GatewayError < RuntimeError; end
431
483
  class DestroyWithOrdersError < StandardError; end
432
484
  end
@@ -444,14 +496,12 @@ require 'spree/localized_number'
444
496
  require 'spree/money'
445
497
  require 'spree/permitted_attributes'
446
498
  require 'spree/service_module'
447
- require 'spree/database_type_utilities'
448
499
  require 'spree/analytics'
449
500
  require 'spree/events'
450
501
  require 'spree/webhooks'
451
502
 
452
503
  require 'spree/core/partials'
453
504
  require 'spree/core/controller_helpers/auth'
454
- require 'spree/core/controller_helpers/common'
455
505
  require 'spree/core/controller_helpers/order'
456
506
  require 'spree/core/controller_helpers/store'
457
507
  require 'spree/core/controller_helpers/strong_parameters'
@@ -462,6 +512,7 @@ require 'spree/core/controller_helpers/turbo'
462
512
  require 'spree/core/preferences/store'
463
513
  require 'spree/core/preferences/scoped_store'
464
514
  require 'spree/core/preferences/runtime_configuration'
515
+ require 'spree/core/preferences/masking'
465
516
 
466
517
  require 'spree/core/permission_configuration'
467
518
  require 'spree/core/ransack_configuration'
@@ -56,6 +56,7 @@ module Spree
56
56
  :shipment_attributes,
57
57
  :shipping_method_attributes,
58
58
  :shipping_category_attributes,
59
+ :channel_attributes,
59
60
  :source_attributes,
60
61
  :stock_item_attributes,
61
62
  :stock_location_attributes,
@@ -90,10 +91,9 @@ module Spree
90
91
 
91
92
  @@allowed_origin_attributes = [:origin]
92
93
 
93
- @@api_key_attributes = [:name, :key_type]
94
+ @@api_key_attributes = [:name, :key_type, { scopes: [] }]
94
95
 
95
- @@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position,
96
- :media_type, :focal_point_x, :focal_point_y, :external_video_url]
96
+ @@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position, :url, :signed_id]
97
97
 
98
98
  @@checkout_attributes = [
99
99
  :coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing, :use_shipping,
@@ -169,7 +169,7 @@ module Spree
169
169
 
170
170
  @@payment_attributes = [:amount, :payment_method_id, :payment_method]
171
171
 
172
- @@payment_method_attributes = [:name, :type, :description, :active, :display_on, :auto_capture, :position]
172
+ @@payment_method_attributes = [:name, :type, :description, :active, :display_on, :auto_capture, :position, { metadata: {}, preferences: {} }]
173
173
 
174
174
  @@payment_session_attributes = [:amount, :payment_method_id, { external_data: {} }]
175
175
 
@@ -192,8 +192,8 @@ module Spree
192
192
  label_list: [],
193
193
  option_type_ids: [],
194
194
  taxon_ids: [],
195
- store_ids: [],
196
- product_option_types_attributes: [:id, :option_type_id, :position, :_destroy]
195
+ product_option_types_attributes: [:id, :option_type_id, :position, :_destroy],
196
+ legacy_product_publications_attributes: [:id, :channel_id, :published_at, :unpublished_at, :_destroy]
197
197
  }
198
198
  ]
199
199
 
@@ -240,6 +240,8 @@ module Spree
240
240
 
241
241
  @@shipping_category_attributes = [:name]
242
242
 
243
+ @@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy]
244
+
243
245
  @@shipping_method_attributes = [:name, :admin_name, :code, :tracking_url, :tax_category_id, :display_on,
244
246
  :estimated_transit_business_days_min, :estimated_transit_business_days_max,
245
247
  :calculator_type, :preferences, zone_ids: [], shipping_category_ids: [], calculator_attributes: {}]
@@ -251,7 +253,7 @@ module Spree
251
253
  :gateway_payment_profile_id, :last_digits, :name, :encrypted_data
252
254
  ]
253
255
 
254
- @@stock_item_attributes = [:variant_id, :stock_location_id, :backorderable, :count_on_hand]
256
+ @@stock_item_attributes = [:variant_id, :stock_location_id, :backorderable, :count_on_hand, { metadata: {} }]
255
257
 
256
258
  @@stock_location_attributes = [
257
259
  :name, :active, :address1, :address2, :city, :zipcode, :company,
@@ -5,19 +5,26 @@ FactoryBot.define do
5
5
  company { 'Company' }
6
6
  sequence(:address1) { |n| "#{n} Lovely Street" }
7
7
  address2 { 'Northwest' }
8
- city { 'Herndon' }
9
- zipcode { '35005' }
8
+ city { 'New York' }
9
+ zipcode { '10118' }
10
10
  phone { '555-555-0199' }
11
11
  alternative_phone { '555-555-0199' }
12
12
 
13
- state { |address| address.association(:state) || Spree::State.last }
14
-
15
- country do |address|
16
- if address.state
17
- address.state.country
18
- else
19
- address.association(:country)
13
+ # Default to a real US/NY pair (cached via find_or_create_by) so generated
14
+ # OpenAPI examples carry plausible country/state fields. Tests that need a
15
+ # different state/country pass them explicitly.
16
+ country do
17
+ Spree::Country.find_or_create_by!(iso: 'US') do |c|
18
+ c.iso3 = 'USA'
19
+ c.name = 'United States of America'
20
+ c.iso_name = 'UNITED STATES'
21
+ c.numcode = 840
22
+ c.states_required = true
20
23
  end
21
24
  end
25
+
26
+ state do |address|
27
+ (address.country || Spree::Country.find_by(iso: 'US'))&.states&.find_or_create_by!(abbr: 'NY') { |s| s.name = 'New York' }
28
+ end
22
29
  end
23
30
  end
@@ -10,6 +10,7 @@ FactoryBot.define do
10
10
 
11
11
  trait :secret do
12
12
  key_type { 'secret' }
13
+ scopes { ['write_all'] }
13
14
  end
14
15
 
15
16
  trait :revoked do
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :channel, class: Spree::Channel do
3
+ store { Spree::Store.default || association(:store) }
4
+ sequence(:name) { |n| "Channel #{n}" }
5
+ sequence(:code) { |n| "channel_#{n}" }
6
+ active { true }
7
+ end
8
+ end
@@ -8,14 +8,8 @@ FactoryBot.define do
8
8
  product { nil }
9
9
  end
10
10
  variant do
11
- resolved_product = product || begin
12
- if order&.store&.present?
13
- create(:product, stores: [order.store])
14
- else
15
- create(:product)
16
- end
17
- end
18
- resolved_product.master
11
+ resolved_product = product || create(:product)
12
+ resolved_product.default_variant
19
13
  end
20
14
  end
21
15
  end
@@ -3,6 +3,8 @@ FactoryBot.define do
3
3
  email { FFaker::Internet.unique.email }
4
4
  verified_at { nil }
5
5
 
6
+ store { Spree::Current.store || create(:store) }
7
+
6
8
  trait :with_user do
7
9
  association :user, factory: :user
8
10
  end
@@ -4,12 +4,10 @@ FactoryBot.define do
4
4
  description { generate(:random_description) }
5
5
  cost_price { 17.00 }
6
6
  sku { generate(:sku) }
7
- available_on { 1.year.ago }
8
- make_active_at { 1.year.ago }
9
7
  deleted_at { nil }
10
8
  shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) }
11
9
  status { 'active' }
12
- stores { [Spree::Store.default] }
10
+ store { Spree::Store.default || association(:store) }
13
11
 
14
12
  transient do
15
13
  price { 19.99 }
@@ -17,11 +15,8 @@ FactoryBot.define do
17
15
  currency { nil }
18
16
  end
19
17
 
20
- # ensure stock item will be created for this products master
21
- # also attach this product to the default store if no stores are passed in
22
18
  before(:create) do |_product|
23
19
  create(:stock_location) unless Spree::StockLocation.any?
24
- create(:store, default: true) unless Spree::Store.any?
25
20
  end
26
21
  after(:create) do |product, evaluator|
27
22
  existing_location_ids = product.master.stock_items.pluck(:stock_location_id)
@@ -30,9 +25,23 @@ FactoryBot.define do
30
25
  end
31
26
 
32
27
  if evaluator.price.present?
33
- price_currency = evaluator.currency || product.stores.first&.default_currency || 'USD'
28
+ price_currency = evaluator.currency || product.store&.default_currency || 'USD'
34
29
  product.master.set_price(price_currency, evaluator.price, evaluator.compare_at_price)
35
30
  end
31
+
32
+ # Test convenience only: auto-publish each product on its store's
33
+ # default channel so legacy spec assertions that depend on
34
+ # current-channel visibility (.active, .available, .not_discontinued)
35
+ # keep passing. Production callers must publish explicitly via the
36
+ # Admin SDK / Dashboard create form.
37
+ if product.store&.default_channel && product.product_publications.empty?
38
+ Spree::ProductPublication.create!(
39
+ product: product,
40
+ channel: product.store.default_channel,
41
+ published_at: product.available_on,
42
+ unpublished_at: product.discontinue_on
43
+ )
44
+ end
36
45
  end
37
46
 
38
47
  factory :custom_product do
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :product_publication, class: Spree::ProductPublication do
3
+ product
4
+ channel { product&.store&.default_channel || association(:channel) }
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ FactoryBot.define do
2
+ factory :refresh_token, class: 'Spree::RefreshToken' do
3
+ association :user, factory: :user
4
+ user_type { user.class.to_s }
5
+ expires_at { Spree::RefreshToken.default_expiry.from_now }
6
+
7
+ trait :for_admin do
8
+ association :user, factory: :admin_user
9
+ end
10
+
11
+ trait :expired do
12
+ expires_at { 1.minute.ago }
13
+ end
14
+ end
15
+ end
@@ -19,8 +19,8 @@ FactoryBot.define do
19
19
  # variant will add itself to all stock_locations in an after_create
20
20
  # creating a product will automatically create a master variant
21
21
  store = Spree::Store.first || create(:store)
22
- product_1 = create(:product, stores: [store])
23
- product_2 = create(:product, stores: [store])
22
+ product_1 = create(:product)
23
+ product_2 = create(:product)
24
24
 
25
25
  stock_location.stock_item_or_create(product_1.master).adjust_count_on_hand(10)
26
26
  stock_location.stock_item_or_create(product_2.master).adjust_count_on_hand(20)
@@ -0,0 +1,31 @@
1
+ FactoryBot.define do
2
+ factory :stock_reservation, class: Spree::StockReservation do
3
+ quantity { 1 }
4
+ expires_at { 10.minutes.from_now }
5
+
6
+ transient do
7
+ order { nil }
8
+ end
9
+
10
+ # Build the order first (with at least one line_item), then derive
11
+ # stock_item from that line_item's variant so the three FKs reference the
12
+ # same variant. Callers can override stock_item:/line_item:/order: to wire
13
+ # up a specific scenario.
14
+ after(:build) do |reservation, evaluator|
15
+ reservation.order ||= evaluator.order || create(:order_with_line_items, line_items_count: 1)
16
+
17
+ if reservation.line_item.nil?
18
+ reservation.line_item = reservation.order.line_items.first ||
19
+ create(:line_item, order: reservation.order)
20
+ reservation.order.line_items.reload
21
+ end
22
+
23
+ reservation.stock_item ||= reservation.line_item.variant.stock_items.first ||
24
+ create(:stock_item, variant: reservation.line_item.variant)
25
+ end
26
+
27
+ trait :expired do
28
+ expires_at { 1.minute.ago }
29
+ end
30
+ end
31
+ end
@@ -11,7 +11,7 @@ FactoryBot.define do
11
11
  is_master { 0 }
12
12
  track_inventory { true }
13
13
 
14
- product { |p| p.association(:base_product, stores: [Spree::Store.default]) }
14
+ product { |p| p.association(:base_product) }
15
15
  option_values { [build(:option_value)] }
16
16
 
17
17
  transient do
@@ -35,14 +35,14 @@ FactoryBot.define do
35
35
  end
36
36
 
37
37
  if evaluator.price.present?
38
- price_currency = evaluator.currency || variant.product&.stores&.first&.default_currency || 'USD'
38
+ price_currency = evaluator.currency || variant.product&.store&.default_currency || 'USD'
39
39
  variant.set_price(price_currency, evaluator.price, evaluator.compare_at_price)
40
40
  end
41
41
  end
42
42
 
43
43
  factory :variant do
44
44
  # on_hand 5
45
- product { |p| p.association(:product, stores: [Spree::Store.default]) }
45
+ product { |p| p.association(:product) }
46
46
 
47
47
  factory :with_image_variant do
48
48
  images { create_list(:image, 1) }
@@ -4,7 +4,7 @@ class OrderWalkthrough
4
4
 
5
5
  # A payment method must exist for an order to proceed through the Address state
6
6
  unless Spree::PaymentMethod.exists?
7
- FactoryBot.create(:check_payment_method, stores: [store])
7
+ FactoryBot.create(:check_payment_method)
8
8
  end
9
9
 
10
10
  # Need to create a valid zone too...
@@ -31,6 +31,16 @@ RSpec.configure do |config|
31
31
  @default_store&.promotions = []
32
32
  @default_store&.update_column(:checkout_zone_id, nil) if @default_store&.read_attribute(:checkout_zone_id).present?
33
33
  @default_store&.payment_methods = []
34
+ # The shared +@default_store+ Ruby object lives across the whole
35
+ # +before(:all)+ block, so AR association caches (+default_market+,
36
+ # +channels+, etc.) and per-instance memos (+@has_markets+) need to
37
+ # be cleared between examples or stale +nil+s leak across tests.
38
+ if @default_store
39
+ @default_store.association(:default_market).reset if @default_store.association_cached?(:default_market)
40
+ @default_store.association(:markets).reset if @default_store.association_cached?(:markets)
41
+ @default_store.remove_instance_variable(:@has_markets) if @default_store.instance_variable_defined?(:@has_markets)
42
+ @default_store.reload
43
+ end
34
44
  end
35
45
  end
36
46
 
@@ -0,0 +1,53 @@
1
+ # Spree 5.4 → 5.5 upgrade manifest.
2
+ #
3
+ # Lists ONLY the version-specific rake tasks that perform data backfills.
4
+ # Universal upgrade steps (bundle update, db:migrate, scheduling cron jobs,
5
+ # reviewing breaking changes) are NOT in this file:
6
+ #
7
+ # - `bundle update` + `db:migrate` are handled by your deploy pipeline
8
+ # (Heroku release phase, K8s init container, Render auto-migrate,
9
+ # Capistrano deploy hook, etc.) and by the @spree/cli's `spree upgrade`
10
+ # wrapper for local development.
11
+ # - Cron scheduling, optional tunings, and human-readable behavior changes
12
+ # live in the upgrade doc at docs/developer/upgrades/5.4-to-5.5.mdx.
13
+ #
14
+ # This manifest is the machine-runnable shape — what `bin/rake spree:upgrade`
15
+ # (in production) and `spree upgrade` (in dev) execute. Every step must be a
16
+ # rake task and must be idempotent. Re-running the full manifest is safe.
17
+ ---
18
+ from: "5.4"
19
+ to: "5.5"
20
+ docs: "https://spreecommerce.org/docs/developer/upgrades/5.4-to-5.5"
21
+
22
+ steps:
23
+ - id: media
24
+ name: "Migrate legacy variant-pinned images to product-level media"
25
+ task: "spree:media:migrate_master_images_to_product_media"
26
+ notes: |
27
+ Enqueues one `Spree::Media::MigrateProductAssetsJob` per product onto
28
+ the `images` queue — confirm your job runner is processing that queue.
29
+ Storefront keeps working while jobs drain (old assets stay variant-pinned
30
+ until each job finishes). For large catalogs, tune with BATCH_SIZE=1000.
31
+
32
+ - id: channels
33
+ name: "Run the Channels upgrade (creates default channels, publications, order channel ids)"
34
+ task: "spree:channels:upgrade"
35
+ notes: |
36
+ Aggregator that runs four sub-tasks in order:
37
+ 1. spree:channels:create_defaults
38
+ 2. spree:upgrade:populate_publications
39
+ 3. spree:channels:backfill_order_channel_ids
40
+ 4. spree:channels:backfill_product_publication_dates
41
+ Until this runs, every product has store_id IS NULL and is invisible
42
+ to Product.for_store — admin lists, storefront catalog, and search
43
+ indexer all return empty.
44
+
45
+ - id: reindex
46
+ name: "Reindex products against the configured search provider"
47
+ task: "spree:search:reindex"
48
+ notes: |
49
+ No-op for the default Database provider. For Meilisearch (or any other
50
+ external search provider), this is required after the channels upgrade
51
+ because products only become visible to Product.for_store once they
52
+ have a store_id — reindexing before the channels step would index 0
53
+ products. Must run AFTER `channels`.
@@ -0,0 +1,94 @@
1
+ namespace :spree do
2
+ namespace :channels do
3
+ desc 'Create the default channel for every existing store (idempotent — calls Store#ensure_default_channel).'
4
+ task create_defaults: :environment do
5
+ created = 0
6
+ Spree::Store.find_each do |store|
7
+ next if store.default_channel
8
+
9
+ store.ensure_default_channel
10
+ created += 1
11
+ puts " Created default channel for store '#{store.name}'"
12
+ end
13
+
14
+ puts created.zero? ? ' All stores already have a default channel.' : " Created #{created} default channel(s)."
15
+ end
16
+
17
+ desc 'Backfill spree_orders.channel_id from the legacy spree_orders.channel string column'
18
+ task backfill_order_channel_ids: :environment do
19
+ # Idempotent: only touches orders where channel_id is nil. Safe to
20
+ # re-run after partial completion. Returns gracefully if the legacy
21
+ # string column has already been dropped.
22
+ unless legacy_channel_column?
23
+ puts 'Legacy channel column not present — backfill is unnecessary.'
24
+ next
25
+ end
26
+
27
+ Spree::Store.find_each do |store|
28
+ legacy_codes = Spree::Order.where(store_id: store.id, channel_id: nil)
29
+ .distinct
30
+ .pluck(Arel.sql('channel'))
31
+ .compact_blank
32
+
33
+ codes_to_process = legacy_codes.uniq
34
+ codes_to_process << Spree::Channel::DEFAULT_CODE unless codes_to_process.include?(Spree::Channel::DEFAULT_CODE)
35
+
36
+ codes_to_process.each do |code|
37
+ channel = store.channels.find_or_create_by!(code: code) do |c|
38
+ c.name = code.titleize
39
+ end
40
+
41
+ scope = Spree::Order.where(store_id: store.id, channel_id: nil)
42
+ scope = if code == Spree::Channel::DEFAULT_CODE
43
+ # Only the default channel claims NULL/blank rows.
44
+ scope.where(Arel.sql("channel = ? OR channel IS NULL OR channel = ''"), code)
45
+ else
46
+ scope.where(Arel.sql('channel = ?'), code)
47
+ end
48
+
49
+ updated = scope.update_all(channel_id: channel.id)
50
+
51
+ next if updated.zero?
52
+
53
+ puts " Store '#{store.name}': mapped #{updated} orders with channel='#{code}' → #{channel.name} (#{channel.code})"
54
+ end
55
+ end
56
+ end
57
+
58
+ desc 'Backfill published_at and unpublished_at on ProductPublications from the legacy Product.available_on / discontinue_on columns'
59
+ task backfill_product_publication_dates: :environment do
60
+ # Per-product loop (not join-update) for SQLite/MySQL/Postgres portability.
61
+ published = 0
62
+ unpublished = 0
63
+
64
+ products_with_dates = Spree::Product.where.not(available_on: nil).or(Spree::Product.where.not(discontinue_on: nil))
65
+
66
+ products_with_dates.find_each(batch_size: 500) do |product|
67
+ publications = Spree::ProductPublication.where(product_id: product.id)
68
+ # Read raw columns — +product.available_on+ / +product.discontinue_on+
69
+ # go through +Product::Channels+'s reader override which prefers the
70
+ # current-channel publication's date (which is nil pre-backfill).
71
+ legacy_available_on = product[:available_on]
72
+ legacy_discontinue_on = product[:discontinue_on]
73
+
74
+ published += publications.where(published_at: nil).update_all(published_at: legacy_available_on) if legacy_available_on
75
+ unpublished += publications.where(unpublished_at: nil).update_all(unpublished_at: legacy_discontinue_on) if legacy_discontinue_on
76
+ end
77
+
78
+ total = published + unpublished
79
+ puts total.zero? ? ' All product-publication dates already populated.' : " Backfilled dates on #{published} published_at + #{unpublished} unpublished_at column(s)."
80
+ end
81
+
82
+ desc 'Run the full 5.4 → 5.5 channel upgrade: create default channels, backfill products to store_id and publications, backfill order channels, backfill publication date windows'
83
+ task upgrade: [
84
+ :create_defaults,
85
+ 'spree:upgrade:populate_publications',
86
+ :backfill_order_channel_ids,
87
+ :backfill_product_publication_dates
88
+ ]
89
+
90
+ def legacy_channel_column?
91
+ ActiveRecord::Base.connection.column_exists?(:spree_orders, :channel)
92
+ end
93
+ end
94
+ end
data/lib/tasks/cli.rake CHANGED
@@ -12,8 +12,9 @@ namespace :spree do
12
12
  task create_api_key: :environment do
13
13
  name = ENV.fetch('NAME')
14
14
  key_type = ENV.fetch('KEY_TYPE')
15
+ scopes = ENV.fetch('SCOPES', '').split(',').map(&:strip).reject(&:empty?)
15
16
  store = Spree::Store.default
16
- key = store.api_keys.create!(name: name, key_type: key_type)
17
+ key = store.api_keys.create!(name: name, key_type: key_type, scopes: scopes)
17
18
  print key.plaintext_token
18
19
  end
19
20