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,25 @@
1
+ <% if namespace_chain.empty? -%>
2
+ module Spree
3
+ module <%= decorator_name %>
4
+ def self.prepended(base)
5
+ # base.before_action :my_filter
6
+ end
7
+
8
+ # add custom methods here
9
+ end
10
+ end
11
+
12
+ Spree::<%= controller_name %>.prepend Spree::<%= decorator_name %>
13
+ <% else -%>
14
+ module Spree::<%= namespace_chain %>
15
+ module <%= decorator_name %>
16
+ def self.prepended(base)
17
+ # base.before_action :my_filter
18
+ end
19
+
20
+ # add custom methods here
21
+ end
22
+ end
23
+
24
+ <%= prepend_invocation %>
25
+ <% end -%>
@@ -2,20 +2,86 @@ require "rails/generators/active_record/model/model_generator"
2
2
 
3
3
  module Spree
4
4
  class ModelGenerator < ActiveRecord::Generators::ModelGenerator
5
-
6
5
  def self.source_paths
7
- paths = superclass.source_paths
8
- paths << File.expand_path('templates', __dir__)
9
- paths.flatten
6
+ [File.expand_path('templates', __dir__), *superclass.source_paths]
10
7
  end
11
8
 
12
- class_option :parent, type: :string, default: "Spree::Base", desc: "The parent class for the generated model"
9
+ class_option :parent,
10
+ type: :string,
11
+ default: 'Spree.base_class',
12
+ desc: 'The parent class expression for the generated model'
13
+
14
+ class_option :id_prefix,
15
+ type: :string,
16
+ desc: 'Prefix for prefixed IDs (default: snake_cased class name, e.g. brand)'
17
+
18
+ class_option :paranoid,
19
+ type: :boolean,
20
+ default: false,
21
+ desc: 'Enable acts_as_paranoid soft-delete (adds deleted_at column + index)'
22
+
23
+ class_option :metafields,
24
+ type: :boolean,
25
+ default: false,
26
+ desc: 'Include Spree::Metafields and Spree::Metadata concerns'
13
27
 
14
- desc 'Creates a new Spree model'
28
+ desc 'Creates a new Spree model with prefixed IDs and Spree.base_class parent'
15
29
 
16
- # Override to prevent module file from being created
17
30
  def create_module_file
18
31
  return
19
32
  end
33
+
34
+ no_tasks do
35
+ def id_prefix
36
+ options[:id_prefix] || file_name
37
+ end
38
+
39
+ def paranoid?
40
+ options[:paranoid]
41
+ end
42
+
43
+ def metafields?
44
+ options[:metafields]
45
+ end
46
+
47
+ def class_path
48
+ ['spree']
49
+ end
50
+
51
+ def table_name
52
+ "spree_#{super.delete_prefix('spree_')}"
53
+ end
54
+
55
+ # Resolution: explicit class hint in attribute spec → Spree.user_class
56
+ # for user-named columns → Spree.admin_user_class for admin-user-named
57
+ # columns → Spree::<CamelCasedName> default. The Rails attribute parser
58
+ # splits on `:` so explicit hints can only be unqualified names; users
59
+ # needing a namespace edit the generated belongs_to.
60
+ def belongs_to_class_name_expr(attribute)
61
+ explicit = attribute.attr_options.keys.find { |k| k.to_s.match?(/\A(::)?[A-Z]/) }
62
+ return "'#{explicit}'" if explicit
63
+
64
+ case attribute.name
65
+ when 'user', 'user_id'
66
+ '"::#{Spree.user_class}"'
67
+ when 'admin_user', 'admin_user_id', 'created_by', 'created_by_id',
68
+ 'approver', 'approver_id', 'canceler', 'canceler_id'
69
+ '"::#{Spree.admin_user_class}"'
70
+ else
71
+ "'Spree::#{attribute.name.camelize}'"
72
+ end
73
+ end
74
+
75
+ def column_default_for(attribute)
76
+ return nil unless attribute.attr_options.key?(:default)
77
+
78
+ value = attribute.attr_options[:default]
79
+ case value
80
+ when true, false, nil then value.inspect
81
+ when Numeric then value.to_s
82
+ else value.to_s.inspect
83
+ end
84
+ end
85
+ end
20
86
  end
21
87
  end
@@ -0,0 +1,40 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+ <% attributes.reject(&:reference?).reject(&:virtual?).each do |attribute| -%>
5
+ <% if attribute.password_digest? -%>
6
+ t.string :password_digest, null: false
7
+ <% elsif attribute.token? -%>
8
+ t.string :<%= attribute.name %>, null: false
9
+ <% else -%>
10
+ <% default_expr = column_default_for(attribute) -%>
11
+ t.<%= attribute.type %> :<%= attribute.name %>, null: false<%= ", default: #{default_expr}" if default_expr %>
12
+ <% end -%>
13
+ <% end -%>
14
+ <% attributes.select(&:reference?).each do |attribute| -%>
15
+ t.references :<%= attribute.name %>, null: false, index: true, foreign_key: false<%= ', polymorphic: true' if attribute.polymorphic? %>
16
+ <% end -%>
17
+ <% if paranoid? -%>
18
+ t.datetime :deleted_at
19
+
20
+ <% end -%>
21
+ <% if options[:timestamps] -%>
22
+ t.timestamps
23
+ <% end -%>
24
+ end
25
+ <% attributes.select(&:token?).each do |attribute| -%>
26
+ add_index :<%= table_name %>, :<%= attribute.name %>, unique: true,
27
+ name: 'index_<%= table_name %>_on_<%= attribute.name %>'
28
+ <% end -%>
29
+ <% attributes.select(&:has_uniq_index?).reject(&:token?).each do |attribute| -%>
30
+ add_index :<%= table_name %>, :<%= attribute.name %>, unique: true,
31
+ name: 'index_<%= table_name %>_on_<%= attribute.name %>'
32
+ <% end -%>
33
+ <% attributes.select { |a| a.has_index? && !a.has_uniq_index? && !a.reference? && !a.token? }.each do |attribute| -%>
34
+ add_index :<%= table_name %>, :<%= attribute.name %>
35
+ <% end -%>
36
+ <% if paranoid? -%>
37
+ add_index :<%= table_name %>, :deleted_at
38
+ <% end -%>
39
+ end
40
+ end
@@ -1,7 +1,22 @@
1
1
  module Spree
2
- class <%= class_name.demodulize %> < <%= parent_class_name.classify %>
2
+ class <%= class_name.demodulize %> < <%= parent_class_name %>
3
+ <% if metafields? -%>
4
+ include Spree::Metafields
5
+ include Spree::Metadata
6
+
7
+ <% end -%>
8
+ <% if paranoid? -%>
9
+ acts_as_paranoid
10
+
11
+ <% end -%>
12
+ has_prefix_id :<%= id_prefix %>
13
+
3
14
  <% attributes.select(&:reference?).each do |attribute| -%>
4
- belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
15
+ <% if attribute.polymorphic? -%>
16
+ belongs_to :<%= attribute.name %>, polymorphic: true
17
+ <% else -%>
18
+ belongs_to :<%= attribute.name %>, class_name: <%= belongs_to_class_name_expr(attribute) %>
19
+ <% end -%>
5
20
  <% end -%>
6
21
  <% attributes.select(&:rich_text?).each do |attribute| -%>
7
22
  has_rich_text :<%= attribute.name %>
@@ -18,5 +33,16 @@ module Spree
18
33
  <% if attributes.any?(&:password_digest?) -%>
19
34
  has_secure_password
20
35
  <% end -%>
36
+ <% required_attrs = attributes.reject { |a| a.reference? || a.type == :boolean || a.token? || a.attachment? || a.attachments? || a.rich_text? || a.password_digest? } -%>
37
+ <% if required_attrs.any? %>
38
+ <% required_attrs.each do |attr| -%>
39
+ validates :<%= attr.name %>, presence: true<% if attr.has_uniq_index? %>, uniqueness: { scope: spree_base_uniqueness_scope }<% end %>
40
+
41
+ <% end -%>
42
+ <% end -%>
43
+ <% ransackable = attributes.reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }.map(&:name) -%>
44
+ self.whitelisted_ransackable_attributes = %w[<%= ransackable.join(' ') %>]
45
+ self.whitelisted_ransackable_associations = %w[]
46
+ self.whitelisted_ransackable_scopes = %w[]
21
47
  end
22
48
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # spree:subscriber — scaffold an event subscriber and register it.
5
+ #
6
+ # bin/rails g spree:subscriber OmsOrderSync order.completed order.canceled
7
+ # bin/rails g spree:subscriber MyApp::BrandSync brand.created brand.updated
8
+ # bin/rails g spree:subscriber CriticalSync order.completed --sync
9
+ #
10
+ # Subscribers are not auto-discovered — they must be appended to the
11
+ # Spree.subscribers array. Forgetting that step produces a silent no-op,
12
+ # so this generator also maintains the registration in
13
+ # config/initializers/spree.rb with an idempotent line per subscriber.
14
+ class SubscriberGenerator < Rails::Generators::NamedBase
15
+ desc 'Creates a Spree event subscriber and registers it in an initializer'
16
+
17
+ # Every Spree app ships config/initializers/spree.rb (created by
18
+ # spree:install) — registrations go there rather than into a second
19
+ # initializer file.
20
+ INITIALIZER_PATH = 'config/initializers/spree.rb'
21
+ REGISTRATION_ANCHOR = "Rails.application.config.after_initialize do\n"
22
+
23
+ argument :events, type: :array, default: [], banner: 'event.name event.name'
24
+
25
+ class_option :sync,
26
+ type: :boolean,
27
+ default: false,
28
+ desc: 'Run the subscriber synchronously (async: false) instead of via ActiveJob'
29
+
30
+ class_option :skip_spec,
31
+ type: :boolean,
32
+ default: false,
33
+ desc: "Don't generate a spec file"
34
+
35
+ def self.source_paths
36
+ [File.expand_path('templates', __dir__), *superclass.source_paths]
37
+ end
38
+
39
+ def create_subscriber_file
40
+ template 'subscriber.rb.tt', File.join('app/subscribers', class_path, "#{subscriber_file_name}.rb")
41
+ end
42
+
43
+ def register_subscriber
44
+ if File.exist?(destination_path(INITIALIZER_PATH))
45
+ content = File.read(destination_path(INITIALIZER_PATH))
46
+ if content.include?(registration_line.strip)
47
+ say_status :identical, "#{INITIALIZER_PATH} (#{subscriber_class_name} already registered)", :blue
48
+ elsif content.include?(REGISTRATION_ANCHOR)
49
+ inject_into_file INITIALIZER_PATH, registration_line, after: REGISTRATION_ANCHOR
50
+ else
51
+ append_to_file INITIALIZER_PATH, registration_block
52
+ end
53
+ else
54
+ create_file INITIALIZER_PATH, registration_block.lstrip
55
+ end
56
+ end
57
+
58
+ def create_spec_file
59
+ return if options[:skip_spec]
60
+
61
+ template 'subscriber_spec.rb.tt', File.join('spec/subscribers', class_path, "#{subscriber_file_name}_spec.rb")
62
+ end
63
+
64
+ def warn_about_missing_events
65
+ return if events.any?
66
+
67
+ say_status :note, "no events given — edit `subscribes_to` in the generated subscriber (e.g. 'order.completed')", :yellow
68
+ end
69
+
70
+ private
71
+
72
+ # "OmsOrderSync" and "OmsOrderSyncSubscriber" both produce
73
+ # OmsOrderSyncSubscriber / oms_order_sync_subscriber.
74
+ def bare_subscriber_name
75
+ base = class_name.demodulize.sub(/Subscriber\z/, '')
76
+ "#{base}Subscriber"
77
+ end
78
+
79
+ def subscriber_file_name
80
+ bare_subscriber_name.underscore
81
+ end
82
+
83
+ def subscriber_class_name
84
+ (class_path.map { |part| part.camelize } + [bare_subscriber_name]).join('::')
85
+ end
86
+
87
+ def namespace_modules
88
+ class_path.map(&:camelize)
89
+ end
90
+
91
+ def subscribes_to_arguments
92
+ listed = events.any? ? events.map { |e| "'#{e}'" } : ["'TODO.replace_with_event_name'"]
93
+ listed << 'async: false' if options[:sync]
94
+ listed.join(', ')
95
+ end
96
+
97
+ def registration_line
98
+ " Spree.subscribers << #{subscriber_class_name}\n"
99
+ end
100
+
101
+ def registration_block
102
+ <<~RUBY
103
+
104
+ # Event subscribers must be registered explicitly — Spree does not
105
+ # auto-discover classes in app/subscribers/.
106
+ Rails.application.config.after_initialize do
107
+ #{registration_line.chomp}
108
+ end
109
+ RUBY
110
+ end
111
+
112
+ def destination_path(relative)
113
+ File.join(destination_root, relative)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,17 @@
1
+ <% namespace_modules.each_with_index do |mod, i| -%>
2
+ <%= ' ' * i %>module <%= mod %>
3
+ <% end -%>
4
+ <% depth = namespace_modules.size -%>
5
+ <%= ' ' * depth %>class <%= bare_subscriber_name %> < Spree::Subscriber
6
+ <%= ' ' * (depth + 1) %>subscribes_to <%= subscribes_to_arguments %>
7
+
8
+ <%= ' ' * (depth + 1) %>def handle(event)
9
+ <%= ' ' * (depth + 2) %># Event payloads carry prefixed IDs — look records up with find_by_prefix_id:
10
+ <%= ' ' * (depth + 2) %>#
11
+ <%= ' ' * (depth + 2) %># record = Spree::Order.find_by_prefix_id(event.payload['id'])
12
+ <%= ' ' * (depth + 2) %># return unless record
13
+ <%= ' ' * (depth + 1) %>end
14
+ <%= ' ' * depth %>end
15
+ <% namespace_modules.each_with_index do |_, i| -%>
16
+ <%= ' ' * (namespace_modules.size - 1 - i) %>end
17
+ <% end -%>
@@ -0,0 +1,9 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe <%= subscriber_class_name %> do
4
+ it 'handles <%= events.first || 'the subscribed event' %>' do
5
+ event = Spree::Event.new(name: <%= (events.first || 'some.event').inspect %>, payload: { 'id' => 'prefixed_id_here' })
6
+
7
+ expect { described_class.new.handle(event) }.not_to raise_error
8
+ end
9
+ end
@@ -25,6 +25,10 @@ module Spree
25
25
  # Alphabetized to more easily lookup particular preferences
26
26
  preference :address_requires_state, :boolean, default: true, deprecated: true # should state/state_name be required
27
27
  preference :address_requires_phone, :boolean, default: false # Determines whether we require phone in address
28
+ # Origin where the admin SPA is hosted (e.g. `https://admin.shop.com`).
29
+ # Used by admin mailers; falls back to `http://localhost:5173` in dev
30
+ # and the store's storefront URL otherwise. Set this in production.
31
+ preference :admin_url, :string, default: nil
28
32
  preference :allow_checkout_on_gateway_error, :boolean, default: false
29
33
  preference :allow_empty_price_amount, :boolean, default: false
30
34
  preference :allow_guest_checkout, :boolean, default: true, deprecated: true # this is only used in the rails frontend, and is not implemented in API
@@ -88,6 +92,9 @@ module Spree
88
92
  preference :require_master_price, :boolean, default: false
89
93
  preference :restock_inventory, :boolean, default: true # Determines if a return item is restocked automatically once it has been received
90
94
  preference :return_eligibility_number_of_days, :integer, default: 365
95
+ preference :reserve_stock_on, :string, default: 'checkout' # 'checkout' (default) or 'cart' — landing in 6.0 alongside Cart/Order split
96
+ preference :stock_reservations_enabled, :boolean, default: true # Hold stock during checkout to prevent overselling
97
+ preference :default_stock_reservation_ttl_minutes, :integer, default: 10 # Fallback TTL when a Store doesn't override
91
98
  preference :send_core_emails, :boolean, default: true, deprecated: true # Default mail headers settings
92
99
  preference :shipping_instructions, :boolean, deprecated: true
93
100
  preference :show_only_complete_orders_by_default, :boolean, deprecated: true
@@ -3,7 +3,6 @@ module Spree
3
3
  module ControllerHelpers
4
4
  module Auth
5
5
  extend ActiveSupport::Concern
6
- include Spree::Core::TokenGenerator
7
6
 
8
7
  included do
9
8
  if defined?(helper_method)
@@ -20,17 +19,6 @@ module Spree
20
19
  @current_ability ||= Spree.ability_class.new(try_spree_current_user, { store: current_store })
21
20
  end
22
21
 
23
- def current_oauth_token
24
- Spree::Deprecation.warn('Spree::Current.oauth_token is deprecated and will be removed in Spree 5.5')
25
-
26
- get_last_access_token = ->(user) { Spree::OauthAccessToken.active_for(user).where(expires_in: nil).last }
27
- create_access_token = ->(user) { Spree::OauthAccessToken.create!(resource_owner: user) }
28
- user = try_spree_current_user
29
- return unless user
30
-
31
- @current_oauth_token ||= get_last_access_token.call(user) || create_access_token.call(user)
32
- end
33
-
34
22
  # this will work for devise out of the box
35
23
  # for other auth systems you will need to override this method
36
24
  def store_location(location = nil)
@@ -7,7 +7,6 @@ module Spree
7
7
  included do
8
8
  if defined?(helper_method)
9
9
  helper_method :supported_currencies
10
- helper_method :supported_currencies_for_all_stores
11
10
  helper_method :current_currency
12
11
  helper_method :supported_currency?
13
12
  helper_method :currency_param
@@ -38,22 +37,6 @@ module Spree
38
37
  @supported_currencies ||= current_store&.supported_currencies_list
39
38
  end
40
39
 
41
- # Returns the list of supported currencies for all stores.
42
- # @deprecated This method will be removed in Spree 5.5.
43
- # @return [Array<String>] the list of supported currencies, eg. `["USD", "EUR"]`
44
- def supported_currencies_for_all_stores
45
- Spree::Deprecation.warn(
46
- 'supported_currencies_for_all_stores is deprecated and will be removed in Spree 5.5.'
47
- )
48
-
49
- @supported_currencies_for_all_stores ||= begin
50
- (
51
- Spree::Store.pluck(:supported_currencies).map { |c| c&.split(',') }.flatten + Spree::Store.pluck(:default_currency)
52
- ).
53
- compact.uniq.map { |code| ::Money::Currency.find(code.strip) }
54
- end
55
- end
56
-
57
40
  # Checks if the given currency is supported.
58
41
  # @param currency_iso_code [String] the ISO code of the currency, eg. `USD`
59
42
  # @return [Boolean] `true` if the currency is supported, `false` otherwise
@@ -7,7 +7,6 @@ module Spree
7
7
  included do
8
8
  if defined?(helper_method)
9
9
  helper_method :current_order
10
- helper_method :simple_current_order
11
10
  end
12
11
  end
13
12
 
@@ -15,24 +14,6 @@ module Spree
15
14
  @order_token ||= cookies.signed[:token] || params[:order_token]
16
15
  end
17
16
 
18
- # @deprecated Use `current_order` instead. This method will be removed in Spree 5.5.
19
- def simple_current_order
20
- Spree::Deprecation.warn(
21
- 'simple_current_order is deprecated and will be removed in Spree 5.5. Use current_order instead.'
22
- )
23
-
24
- return @simple_current_order if @simple_current_order
25
-
26
- @simple_current_order = find_order_by_token_or_user
27
-
28
- if @simple_current_order
29
- @simple_current_order.last_ip_address = ip_address
30
- return @simple_current_order
31
- else
32
- @simple_current_order = current_store.orders.new
33
- end
34
- end
35
-
36
17
  # The current incomplete order from the token for use in cart and during checkout
37
18
  def current_order(options = {})
38
19
  options[:create_order_if_necessary] ||= false
@@ -44,6 +44,9 @@ module Spree
44
44
  # order
45
45
  order_approve_service: 'Spree::Orders::Approve',
46
46
  order_cancel_service: 'Spree::Orders::Cancel',
47
+ order_complete_service: 'Spree::Orders::Complete',
48
+ order_create_service: 'Spree::Orders::Create',
49
+ order_update_service: 'Spree::Orders::Update',
47
50
  order_updater: 'Spree::OrderUpdater',
48
51
 
49
52
  # shipment
@@ -62,7 +65,7 @@ module Spree
62
65
  posts_sorter: nil,
63
66
  products_sorter: 'Spree::Products::Sort',
64
67
  # paginator
65
- collection_paginator: 'Spree::Shared::Paginate',
68
+ collection_paginator: nil,
66
69
 
67
70
  # coupons
68
71
  # TODO: we should split this service into 2 separate - Add and Remove
@@ -80,7 +83,7 @@ module Spree
80
83
  credit_cards_destroy_service: 'Spree::CreditCards::Destroy',
81
84
 
82
85
  # classifications
83
- classification_reposition_service: 'Spree::Classifications::Reposition',
86
+ classification_reposition_service: nil,
84
87
 
85
88
  # line items
86
89
  line_item_create_service: 'Spree::LineItems::Create',
@@ -11,6 +11,7 @@ module Spree
11
11
  :payment_methods,
12
12
  :adjusters,
13
13
  :stock_splitters,
14
+ :order_routing,
14
15
  :promotions,
15
16
  :pricing,
16
17
  :line_item_comparison_hooks,
@@ -25,6 +26,7 @@ module Spree
25
26
  :page_blocks,
26
27
  :reports,
27
28
  :translatable_resources,
29
+ :taggable_types,
28
30
  :metafields,
29
31
  :analytics_events,
30
32
  :analytics_event_handlers,
@@ -35,6 +37,7 @@ module Spree
35
37
  SpreeCalculators = Struct.new(:shipping_methods, :tax_rates, :promotion_actions_create_adjustments, :promotion_actions_create_item_adjustments)
36
38
  PromoEnvironment = Struct.new(:rules, :actions)
37
39
  PricingEnvironment = Struct.new(:rules)
40
+ OrderRoutingEnvironment = Struct.new(:strategies, :rules)
38
41
  SpreeValidators = Struct.new(:addresses)
39
42
  MetafieldsEnvironment = Struct.new(:types, :enabled_resources)
40
43
  isolate_namespace Spree
@@ -75,6 +78,15 @@ module Spree
75
78
  initializer 'spree.register.adjustable_adjusters' do |app|
76
79
  end
77
80
 
81
+ # Seed the order routing registries early so engines and apps can append
82
+ # their own strategies / rule kinds from initializer files. Core's defaults
83
+ # are concatenated in after_initialize below.
84
+ initializer 'spree.register.order_routing', before: :load_config_initializers do |app|
85
+ app.config.spree.order_routing = OrderRoutingEnvironment.new
86
+ app.config.spree.order_routing.strategies = []
87
+ app.config.spree.order_routing.rules = []
88
+ end
89
+
78
90
  initializer 'spree.register.metafields' do |app|
79
91
  app.config.spree.metafields = MetafieldsEnvironment.new
80
92
  app.config.spree.metafields.types = []
@@ -133,6 +145,23 @@ module Spree
133
145
  Spree::Adjustable::Adjuster::Tax
134
146
  ]
135
147
 
148
+ # Selectable order routing strategies. The internal Reducer collaborator
149
+ # is intentionally NOT listed — it is not a Strategy::Base. Plugins add
150
+ # their own (or remove Legacy) via this array.
151
+ Rails.application.config.spree.order_routing.strategies.concat [
152
+ Spree::OrderRouting::Strategy::Rules,
153
+ Spree::OrderRouting::Strategy::Legacy
154
+ ]
155
+
156
+ # Available order routing rule kinds. STI dispatches at runtime via the
157
+ # +type+ column; this array is the curated allowlist that drives admin
158
+ # pickers and the rule +type+ validation. Plugins append their own.
159
+ Rails.application.config.spree.order_routing.rules.concat [
160
+ Spree::OrderRouting::Rules::PreferredLocation,
161
+ Spree::OrderRouting::Rules::MinimizeSplits,
162
+ Spree::OrderRouting::Rules::DefaultLocation
163
+ ]
164
+
136
165
  Rails.application.config.spree.calculators.promotion_actions_create_adjustments = [
137
166
  Spree::Calculator::FlatPercentItemTotal,
138
167
  Spree::Calculator::FlatRate,
@@ -161,12 +190,18 @@ module Spree
161
190
  Spree::Promotion::Rules::OptionValue,
162
191
  ]
163
192
 
193
+ # Default registry. MarketRule is included so existing installs
194
+ # don't lose access to saved rule rows in the admin. ZoneRule is
195
+ # intentionally excluded — Zones are being removed in 6.0 (see
196
+ # docs/plans/6.0-tax-provider.md) and we don't want to invest
197
+ # in admin UI for a model on its way out. The class itself
198
+ # stays so legacy data continues to load; it just doesn't show
199
+ # up in the "Add rule" picker.
164
200
  Rails.application.config.spree.pricing.rules.concat [
165
- Spree::PriceRules::ZoneRule,
166
- Spree::PriceRules::MarketRule,
167
201
  Spree::PriceRules::UserRule,
168
202
  Spree::PriceRules::CustomerGroupRule,
169
- Spree::PriceRules::VolumeRule
203
+ Spree::PriceRules::VolumeRule,
204
+ Spree::PriceRules::MarketRule
170
205
  ]
171
206
 
172
207
  Rails.application.config.spree.promotions.actions = [
@@ -216,6 +251,18 @@ module Spree
216
251
  Spree::Policy
217
252
  ]
218
253
 
254
+ # Resources that expose tags via `acts_as_taggable_on :tags`. The
255
+ # Admin API's `/tags` autocomplete endpoint accepts these as
256
+ # `taggable_type`, and the SPA `<TagCombobox>` targets them by name.
257
+ # Extend in an app initializer (after :load_config_initializers) to
258
+ # surface custom taggables — e.g.
259
+ # Rails.application.config.spree.taggable_types << 'MyApp::Vendor'.
260
+ Rails.application.config.spree.taggable_types = [
261
+ 'Spree::Product',
262
+ 'Spree::Order',
263
+ Spree.user_class.to_s
264
+ ]
265
+
219
266
  Rails.application.config.spree.metafields.types = [
220
267
  Spree::Metafields::ShortText,
221
268
  Spree::Metafields::LongText,
@@ -301,12 +348,12 @@ module Spree
301
348
  ]
302
349
 
303
350
  # Pre-load authentication strategy classes to avoid reflection at request time
304
- Rails.application.config.spree.store_authentication_strategies = {
351
+ Rails.application.config.spree.store_authentication_strategies = Spree::Authentication::StrategyRegistry.new(
305
352
  email: Spree::Authentication::Strategies::EmailPasswordStrategy
306
- }
307
- Rails.application.config.spree.admin_authentication_strategies = {
353
+ )
354
+ Rails.application.config.spree.admin_authentication_strategies = Spree::Authentication::StrategyRegistry.new(
308
355
  email: Spree::Authentication::Strategies::EmailPasswordStrategy
309
- }
356
+ )
310
357
  end
311
358
 
312
359
  initializer 'spree.promo.register.promotions.actions' do |app|
@@ -105,6 +105,21 @@ module Spree
105
105
  @role_permissions = {}
106
106
  end
107
107
 
108
+ # Returns a deep copy of this configuration. Useful for snapshotting
109
+ # state before a test mutates it; restore with {#replace}.
110
+ def dup
111
+ copy = super
112
+ copy.instance_variable_set(:@role_permissions, @role_permissions.deep_dup)
113
+ copy
114
+ end
115
+
116
+ # Replaces this configuration's state with a deep copy of `other`'s.
117
+ # Pair with {#dup} to snapshot/restore around code that mutates the
118
+ # global Spree.permissions config.
119
+ def replace(other)
120
+ @role_permissions = other.instance_variable_get(:@role_permissions).deep_dup
121
+ end
122
+
108
123
  private
109
124
 
110
125
  def normalize_role_name(role_name)