spree_core 6.0.0.beta1 → 6.0.0.beta3

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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
  3. data/app/jobs/spree/sample_data/load_job.rb +18 -0
  4. data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
  5. data/app/mailers/spree/admin_user_mailer.rb +2 -2
  6. data/app/models/concerns/spree/acted_by.rb +210 -0
  7. data/app/models/concerns/spree/actor.rb +51 -0
  8. data/app/models/concerns/spree/admin_user_methods.rb +48 -17
  9. data/app/models/concerns/spree/customer_methods.rb +17 -1
  10. data/app/models/concerns/spree/has_number.rb +3 -2
  11. data/app/models/concerns/spree/prefixed_id.rb +4 -1
  12. data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
  13. data/app/models/concerns/spree/purchase/totals.rb +3 -1
  14. data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
  15. data/app/models/concerns/spree/translatable_resource.rb +6 -7
  16. data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
  17. data/app/models/spree/adjusters/promotion.rb +1 -1
  18. data/app/models/spree/api_key.rb +5 -3
  19. data/app/models/spree/authentication/lockout.rb +34 -0
  20. data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
  21. data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
  22. data/app/models/spree/catalog.rb +50 -13
  23. data/app/models/spree/claim.rb +3 -1
  24. data/app/models/spree/commission_line.rb +4 -2
  25. data/app/models/spree/commission_rule.rb +1 -1
  26. data/app/models/spree/coupon_code.rb +14 -2
  27. data/app/models/spree/current.rb +56 -13
  28. data/app/models/spree/exchange.rb +3 -1
  29. data/app/models/spree/export.rb +8 -5
  30. data/app/models/spree/exports/customers.rb +2 -2
  31. data/app/models/spree/fulfillment_group.rb +127 -0
  32. data/app/models/spree/gift_card.rb +9 -0
  33. data/app/models/spree/invitation.rb +18 -5
  34. data/app/models/spree/line_item.rb +1 -1
  35. data/app/models/spree/number_generators/sequential.rb +2 -2
  36. data/app/models/spree/option_value.rb +7 -0
  37. data/app/models/spree/order.rb +89 -18
  38. data/app/models/spree/order_group.rb +78 -6
  39. data/app/models/spree/payment_source.rb +1 -1
  40. data/app/models/spree/payment_split.rb +24 -0
  41. data/app/models/spree/price.rb +74 -0
  42. data/app/models/spree/pricing_provider/internal.rb +37 -15
  43. data/app/models/spree/product.rb +4 -0
  44. data/app/models/spree/product_option_type.rb +11 -0
  45. data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
  46. data/app/models/spree/promotion/rules/category.rb +14 -9
  47. data/app/models/spree/promotion/rules/product.rb +17 -1
  48. data/app/models/spree/promotion.rb +1 -1
  49. data/app/models/spree/promotion_action.rb +12 -1
  50. data/app/models/spree/promotion_handler/coupon.rb +50 -5
  51. data/app/models/spree/promotion_rule.rb +14 -0
  52. data/app/models/spree/refund.rb +2 -1
  53. data/app/models/spree/return.rb +18 -7
  54. data/app/models/spree/return_line_item.rb +19 -0
  55. data/app/models/spree/search_provider/database.rb +10 -2
  56. data/app/models/spree/seller_payout.rb +3 -1
  57. data/app/models/spree/seller_transfer.rb +3 -1
  58. data/app/models/spree/stock_receipt.rb +7 -2
  59. data/app/models/spree/store.rb +4 -0
  60. data/app/models/spree/store_credit.rb +4 -0
  61. data/app/models/spree/translations/batch.rb +4 -2
  62. data/app/models/spree/user_identity.rb +10 -6
  63. data/app/models/spree/variant.rb +8 -3
  64. data/app/models/spree/webhook_delivery.rb +28 -0
  65. data/app/models/spree/webhook_endpoint.rb +14 -0
  66. data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
  67. data/app/services/spree/carts/split_by_seller.rb +22 -12
  68. data/app/services/spree/companies/add_member.rb +8 -2
  69. data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
  70. data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
  71. data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
  72. data/app/services/spree/order_groups/allocate_payments.rb +3 -0
  73. data/app/services/spree/orders/approve.rb +3 -2
  74. data/app/services/spree/orders/build_fulfillments.rb +6 -0
  75. data/app/services/spree/orders/update_statuses.rb +8 -2
  76. data/app/services/spree/prices/bulk_upsert.rb +186 -47
  77. data/app/services/spree/sample_data/import_builder.rb +1 -1
  78. data/app/services/spree/sample_data/loader.rb +54 -40
  79. data/app/services/spree/seeds/all.rb +1 -26
  80. data/app/services/spree/seeds/allowed_origins.rb +5 -4
  81. data/app/services/spree/seeds/api_keys.rb +10 -9
  82. data/app/services/spree/seeds/channels.rb +9 -8
  83. data/app/services/spree/seeds/commission_rates.rb +21 -20
  84. data/app/services/spree/seeds/customer_groups.rb +5 -4
  85. data/app/services/spree/seeds/digital_delivery.rb +7 -8
  86. data/app/services/spree/seeds/payment_methods.rb +12 -11
  87. data/app/services/spree/seeds/product_types.rb +7 -8
  88. data/app/services/spree/seeds/returns_environment.rb +12 -11
  89. data/app/services/spree/seeds/roles.rb +8 -7
  90. data/app/services/spree/seeds/saved_reports.rb +19 -20
  91. data/app/services/spree/seeds/seller_requirements.rb +5 -4
  92. data/app/services/spree/seeds/store_resources.rb +41 -0
  93. data/app/services/spree/seeds/store_scoped.rb +15 -0
  94. data/app/services/spree/seeds/tax_categories.rb +10 -9
  95. data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
  96. data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
  97. data/app/subscribers/spree/order_status_subscriber.rb +15 -7
  98. data/app/workflows/spree/carts/add_item.rb +5 -7
  99. data/app/workflows/spree/carts/complete.rb +20 -64
  100. data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
  101. data/app/workflows/spree/claims/create.rb +31 -1
  102. data/app/workflows/spree/claims/resolve.rb +7 -11
  103. data/app/workflows/spree/exchanges/create.rb +11 -0
  104. data/app/workflows/spree/exchanges/fulfill.rb +21 -13
  105. data/app/workflows/spree/orders/cancel.rb +3 -2
  106. data/app/workflows/spree/orders/complete.rb +99 -2
  107. data/app/workflows/spree/price_lists/update.rb +17 -0
  108. data/app/workflows/spree/refunds/create.rb +1 -1
  109. data/app/workflows/spree/refunds/order_payments.rb +55 -1
  110. data/app/workflows/spree/returns/approve.rb +1 -1
  111. data/app/workflows/spree/returns/create.rb +11 -17
  112. data/app/workflows/spree/returns/refund.rb +16 -25
  113. data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
  114. data/app/workflows/spree/sellers/invite.rb +1 -1
  115. data/config/locales/en.yml +8 -119
  116. data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
  117. data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
  118. data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
  119. data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
  120. data/db/sample_data/channels.rb +1 -1
  121. data/db/sample_data/collections.rb +1 -1
  122. data/db/sample_data/custom_field_definitions.rb +1 -1
  123. data/db/sample_data/digital_products.rb +1 -1
  124. data/db/sample_data/fulfillment.rb +1 -1
  125. data/db/sample_data/options.rb +4 -2
  126. data/db/sample_data/orders.rb +9 -10
  127. data/db/sample_data/payment_methods.rb +18 -18
  128. data/db/sample_data/product_type_categories.rb +1 -1
  129. data/db/sample_data/product_types.rb +2 -2
  130. data/db/sample_data/promotions.rb +2 -4
  131. data/db/sample_data/wholesale.rb +1 -1
  132. data/lib/generators/spree/api_resource/api_resource_generator.rb +121 -6
  133. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +8 -2
  134. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
  135. data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
  136. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
  137. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
  138. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  139. data/lib/generators/spree/model/model_generator.rb +40 -1
  140. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +9 -0
  141. data/lib/generators/spree/model/templates/model.rb.tt +10 -4
  142. data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
  143. data/lib/spree/core/configuration.rb +25 -25
  144. data/lib/spree/core/engine.rb +35 -8
  145. data/lib/spree/core/preferences/runtime_configuration.rb +76 -2
  146. data/lib/spree/core/version.rb +1 -1
  147. data/lib/spree/core.rb +27 -0
  148. data/lib/spree/iso_data.rb +23 -4
  149. data/lib/spree/store_scope_guard.rb +16 -1
  150. data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
  151. data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
  152. data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
  153. data/lib/spree/testing_support/factories/user_factory.rb +0 -1
  154. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
  155. data/lib/tasks/backfill_actor_types.rake +35 -0
  156. data/lib/tasks/sample_data.rake +8 -2
  157. data/lib/tasks/store_settings.rake +1 -0
  158. data/lib/tasks/upgrade.rake +6 -21
  159. metadata +23 -9
  160. /data/{app/models → lib}/spree/checkout/default_requirements.rb +0 -0
  161. /data/{app/models → lib}/spree/checkout/registry.rb +0 -0
  162. /data/{app/models → lib}/spree/checkout/requirement.rb +0 -0
  163. /data/{app/models → lib}/spree/checkout/requirements.rb +0 -0
  164. /data/{app/models → lib}/spree/checkout/step.rb +0 -0
@@ -17,6 +17,7 @@ module Spree
17
17
  # - Factory (managed — overwrite on re-run)
18
18
  # - Controller specs (managed — overwrite on re-run)
19
19
  # - Routes (idempotent inject between sentinels)
20
+ # - Permission scope + label (idempotent inject; locale owned-once)
20
21
  #
21
22
  # Owned-once contract: if the model file already exists, the generator
22
23
  # leaves it (and the migration) alone — domain code is yours after
@@ -59,6 +60,11 @@ module Spree
59
60
  default: false,
60
61
  desc: "Don't inject routes into spree/api/config/routes.rb"
61
62
 
63
+ class_option :permission_group,
64
+ type: :string,
65
+ default: 'catalog',
66
+ desc: 'Permission picker group for the read_/write_ keys (orders, catalog, customers, settings, …)'
67
+
62
68
  class_option :skip_specs,
63
69
  type: :boolean,
64
70
  default: false,
@@ -76,7 +82,7 @@ module Spree
76
82
  # the second run with "Spree::Brand is already used in your application".
77
83
  class_option :skip_collision_check, type: :boolean, default: true, desc: false
78
84
 
79
- desc 'Scaffold a complete v3-conformant API resource (model + migration + controllers + serializers + routes + factory + specs)'
85
+ desc 'Creates a new Spree API resource'
80
86
 
81
87
  # --- Owned-once gating ---
82
88
  #
@@ -171,8 +177,10 @@ module Spree
171
177
 
172
178
  routes_file = api_routes_path
173
179
 
180
+ # An installed gem is read-only, which is the normal case: a host app
181
+ # declares its own routes instead, through the engine's route hook.
174
182
  unless File.exist?(routes_file) && File.writable?(routes_file)
175
- say_status :skip, "routes.rb at #{routes_file} (not writable — only edge installs can modify gem source)", :yellow
183
+ draw_app_routes
176
184
  return
177
185
  end
178
186
 
@@ -180,11 +188,96 @@ module Spree
180
188
  inject_route_for(:admin, admin_route_line) if options[:admin]
181
189
  end
182
190
 
191
+ # The Admin controller declares `scoped_resource :<plural>`; without a
192
+ # matching catalog scope its keys cannot be granted to a staff role or
193
+ # minted on a secret API key, so only full-access principals get in.
194
+ def register_permission_scope
195
+ return unless options[:admin]
196
+
197
+ if File.exist?(File.join(destination_root, INITIALIZER_PATH))
198
+ if File.read(File.join(destination_root, INITIALIZER_PATH)).include?("register_scope(:#{plural_name},")
199
+ say_status :identical, "#{INITIALIZER_PATH} (#{plural_name} permission scope)", :blue
200
+ else
201
+ append_to_file INITIALIZER_PATH, permission_scope_block
202
+ end
203
+ else
204
+ create_file INITIALIZER_PATH, permission_scope_block.lstrip
205
+ end
206
+ end
207
+
208
+ def create_permission_locale
209
+ return unless options[:admin]
210
+
211
+ path = "config/locales/spree_#{plural_name}.en.yml"
212
+ if File.exist?(File.join(destination_root, path))
213
+ say_status :skip, "#{path} (owned-once; already exists)", :yellow
214
+ return
215
+ end
216
+
217
+ template 'permissions.en.yml.tt', path
218
+ end
219
+
220
+ no_tasks do
221
+ # A host app declares its API routes in config/routes.rb, inside the
222
+ # engine's route hook that spree-starter ships ready to fill in. The
223
+ # block must be registered before the engine draws its routes, which is
224
+ # why it lives in routes.rb rather than an initializer.
225
+ def draw_app_routes
226
+ path = 'config/routes.rb'
227
+ full = File.join(destination_root, path)
228
+
229
+ unless File.exist?(full)
230
+ say_status :skip, "#{path} not found — add the routes by hand", :yellow
231
+ return
232
+ end
233
+
234
+ unless File.read(full).include?('Spree::Core::Engine.add_routes')
235
+ say_status :skip, "#{path} has no `Spree::Core::Engine.add_routes` block — add the routes by hand", :yellow
236
+ return
237
+ end
238
+
239
+ append_app_route(path, :store, store_route_line.strip) if options[:store]
240
+ append_app_route(path, :admin, admin_route_line.strip) if options[:admin]
241
+ end
242
+
243
+ # Inserts one `resources` line into the named namespace. Idempotent: a
244
+ # resource already declared there is left alone, so re-runs are safe.
245
+ def append_app_route(path, namespace, route_line)
246
+ full = File.join(destination_root, path)
247
+ content = File.read(full)
248
+
249
+ # Confined to the engine's route hook: an application may well have its
250
+ # own `namespace :admin` earlier in the file, and a route inserted there
251
+ # would never be drawn on the engine.
252
+ hook = content[/^(\s*)Spree::Core::Engine\.add_routes do\n.*?^\1end\n/m]
253
+ unless hook
254
+ say_status :skip, "#{path} (no `Spree::Core::Engine.add_routes` block — add `#{route_line}` by hand)", :yellow
255
+ return
256
+ end
257
+
258
+ match = hook.match(/^(\s*)namespace :#{namespace} do\n/)
259
+ unless match
260
+ say_status :skip, "#{path} (no :#{namespace} namespace — add `#{route_line}` by hand)", :yellow
261
+ return
262
+ end
263
+
264
+ block = hook[match.end(0)..].to_s[/\A.*?^#{match[1]}end$/m].to_s
265
+ if block.include?(route_line)
266
+ say_status :identical, "#{path} (#{namespace}: #{route_line})", :blue
267
+ return
268
+ end
269
+
270
+ updated_hook = hook.sub(match[0], "#{match[0]}#{match[1]} #{route_line}\n")
271
+ File.write(full, content.sub(hook, updated_hook))
272
+ say_status :route, "#{path} (#{namespace}: #{route_line})", :green
273
+ end
274
+ end
275
+
183
276
  def print_summary
184
277
  say ''
185
278
  say "✓ Generated Spree::#{bare_class_name} API resource", :green
186
279
  say ''
187
- say " Prefixed ID: #{id_prefix}_xxxxxxxxxx (edit `has_prefix_id` in the model to change)"
280
+ say " Prefixed ID: #{id_prefix}_xxxxxxxxxx"
188
281
  if store_external_name != bare_class_name
189
282
  say " Store API: /api/v3/store/#{store_external_plural} (aliased from #{bare_class_name})"
190
283
  elsif options[:store]
@@ -195,10 +288,14 @@ module Spree
195
288
  say ' Next steps:', :yellow
196
289
  say ' 1. Review the generated model — add validations, scopes, callbacks'
197
290
  say ' 2. Apply the migration: pnpm exec spree migrate'
198
- say ' 3. Set up authorization (CanCanCan ability) for the resource'
199
- say ' 4. Decide whether this resource is store-scoped (add `has_many` on Store)'
291
+ if options[:admin]
292
+ say " 3. Grant read_#{plural_name} / write_#{plural_name} to staff roles or secret API keys"
293
+ else
294
+ say ' 3. Add an Admin API resource later to manage it from the dashboard'
295
+ end
296
+ say " 4. Add `has_many :#{plural_name}` to Spree::Store" if store_scoped?
200
297
  if options[:store] || options[:admin]
201
- say ' 5. Run the specs: pnpm exec spree exec bundle exec rspec spec/controllers/spree/api/v3/'
298
+ say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree rspec spec/controllers/spree/api/v3/"
202
299
  end
203
300
  say ''
204
301
  end
@@ -237,6 +334,13 @@ module Spree
237
334
  file_name.pluralize
238
335
  end
239
336
 
337
+ def permission_group
338
+ group = options[:permission_group].to_s
339
+ raise Thor::Error, "--permission-group must be a lowercase identifier (got #{group.inspect})" unless group.match?(/\A[a-z][a-z0-9_]*\z/)
340
+
341
+ group
342
+ end
343
+
240
344
  # Attributes the controller permits on write. By default, every column
241
345
  # except references (FKs come through nested), attachments, rich text.
242
346
  def permitted_attribute_names
@@ -248,6 +352,17 @@ module Spree
248
352
 
249
353
  private
250
354
 
355
+ INITIALIZER_PATH = 'config/initializers/spree.rb'.freeze
356
+
357
+ def permission_scope_block
358
+ <<~RUBY
359
+
360
+ # Permissions for the #{bare_class_name} API — grants read_#{plural_name} / write_#{plural_name}
361
+ # to staff roles and secret API keys. Labels: config/locales/spree_#{plural_name}.en.yml
362
+ Spree.permissions.register_scope(:#{plural_name}, group: :#{permission_group}, resources: -> { [Spree::#{bare_class_name}] })
363
+ RUBY
364
+ end
365
+
251
366
  # Where the model file lands. We use parent's class_path + file_name so
252
367
  # this stays in sync with whatever Spree::ModelGenerator decides.
253
368
  def model_file_destination
@@ -3,6 +3,12 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class <%= plural_name.camelize %>Controller < ResourceController
6
+ # The permission that authorizes this endpoint — `read_<%= plural_name %>` and
7
+ # `write_<%= plural_name %>`, registered in config/initializers/spree.rb so
8
+ # they can be granted to staff roles and secret API keys. Point it at an
9
+ # existing resource name instead if this endpoint belongs under one.
10
+ scoped_resource :<%= plural_name %>
11
+
6
12
  protected
7
13
 
8
14
  def model_class
@@ -13,8 +19,8 @@ module Spree
13
19
  Spree::Api::V3::Admin::<%= bare_class_name %>Serializer
14
20
  end
15
21
 
16
- def permitted_params
17
- params.permit(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)
22
+ def resource_permitted_attributes
23
+ [<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
18
24
  end
19
25
  end
20
26
  end
@@ -2,6 +2,9 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe Spree::Api::V3::Admin::<%= plural_name.camelize %>Controller, type: :controller do
4
4
  render_views
5
+ # The controller is mounted by the engine, so its paths resolve
6
+ # through the engine's router rather than the application's.
7
+ routes { Spree::Core::Engine.routes }
5
8
 
6
9
  include_context 'API v3 Admin authenticated'
7
10
 
@@ -0,0 +1,6 @@
1
+ en:
2
+ spree:
3
+ permissions_catalog:
4
+ resources:
5
+ <%= plural_name %>:
6
+ label: <%= plural_name.humanize %>
@@ -20,8 +20,8 @@ module Spree
20
20
  <% end -%>
21
21
  <% if writable? -%>
22
22
 
23
- def permitted_params
24
- params.permit(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)
23
+ def resource_permitted_attributes
24
+ [<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
25
25
  end
26
26
  <% end -%>
27
27
  end
@@ -2,8 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe Spree::Api::V3::Store::<%= plural_name.camelize %>Controller, type: :controller do
4
4
  render_views
5
+ # The controller is mounted by the engine, so its paths resolve
6
+ # through the engine's router rather than the application's.
7
+ routes { Spree::Core::Engine.routes }
5
8
 
6
- include_context 'API v3 Store'
9
+ include_context 'API v3 Store guest'
7
10
 
8
11
  let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
9
12
 
@@ -43,5 +43,12 @@ Dummy::Application.configure do
43
43
 
44
44
  config.cache_store = :null_store
45
45
 
46
+ # Spree encrypts secrets at rest (webhook signing keys, OAuth tokens) only when
47
+ # Active Record encryption is configured, so the suite configures it like a
48
+ # production app would.
49
+ config.active_record.encryption.primary_key = 'spree-test-primary-key'
50
+ config.active_record.encryption.deterministic_key = 'spree-test-deterministic-key'
51
+ config.active_record.encryption.key_derivation_salt = 'spree-test-key-derivation-salt'
52
+
46
53
  routes.default_url_options = { host: 'localhost', port: 3000 }
47
54
  end
@@ -25,7 +25,24 @@ module Spree
25
25
  default: false,
26
26
  desc: 'Include Spree::HasCustomFields and Spree::Metadata concerns'
27
27
 
28
- desc 'Creates a new Spree model with prefixed IDs and Spree.base_class parent'
28
+ # On by default so every new resource is observable from the moment it
29
+ # exists — subscribers and webhooks both hang off these events, and a
30
+ # model that publishes nothing is invisible to integrations.
31
+ class_option :lifecycle_events,
32
+ type: :boolean,
33
+ default: true,
34
+ desc: 'Publish created/updated/deleted events'
35
+
36
+ # Store scoping is the default because commerce records belong to a store:
37
+ # catalog, configuration and orders are all per-store. Opt out with
38
+ # --no-store-scoped only for genuinely global reference data (countries,
39
+ # states, roles), which is rare enough to be the explicit case.
40
+ class_option :store_scoped,
41
+ type: :boolean,
42
+ default: true,
43
+ desc: 'Scope the model to a store'
44
+
45
+ desc 'Creates a new Spree model'
29
46
 
30
47
  def create_module_file
31
48
  return
@@ -44,6 +61,28 @@ module Spree
44
61
  options[:custom_fields]
45
62
  end
46
63
 
64
+ def lifecycle_events?
65
+ options[:lifecycle_events]
66
+ end
67
+
68
+ # Whether the record belongs to a store, however that reference got
69
+ # there: the concern below, or a `store:references` the caller passed.
70
+ # Uniqueness and indexes key off this, so a value stays unique within a
71
+ # store rather than across the whole installation.
72
+ def store_scoped?
73
+ options[:store_scoped] || declares_store_reference?
74
+ end
75
+
76
+ # The concern is what adds `belongs_to :store`, so a model that already
77
+ # declares one must not include it as well.
78
+ def includes_store_concern?
79
+ options[:store_scoped] && !declares_store_reference?
80
+ end
81
+
82
+ def declares_store_reference?
83
+ attributes.any? { |attribute| attribute.name == 'store' }
84
+ end
85
+
47
86
  def class_path
48
87
  ['spree']
49
88
  end
@@ -1,6 +1,10 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+ <% if includes_store_concern? -%>
5
+ t.references :store, null: false, index: true, foreign_key: false
6
+
7
+ <% end -%>
4
8
  <% attributes.reject(&:reference?).reject(&:virtual?).each do |attribute| -%>
5
9
  <% if attribute.password_digest? -%>
6
10
  t.string :password_digest, null: false
@@ -27,9 +31,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
27
31
  name: 'index_<%= table_name %>_on_<%= attribute.name %>'
28
32
  <% end -%>
29
33
  <% attributes.select(&:has_uniq_index?).reject(&:token?).each do |attribute| -%>
34
+ <% if store_scoped? -%>
35
+ add_index :<%= table_name %>, [:store_id, :<%= attribute.name %>], unique: true,
36
+ name: 'index_<%= table_name %>_on_store_id_and_<%= attribute.name %>'
37
+ <% else -%>
30
38
  add_index :<%= table_name %>, :<%= attribute.name %>, unique: true,
31
39
  name: 'index_<%= table_name %>_on_<%= attribute.name %>'
32
40
  <% end -%>
41
+ <% end -%>
33
42
  <% attributes.select { |a| a.has_index? && !a.has_uniq_index? && !a.reference? && !a.token? }.each do |attribute| -%>
34
43
  add_index :<%= table_name %>, :<%= attribute.name %>
35
44
  <% end -%>
@@ -1,5 +1,9 @@
1
1
  module Spree
2
2
  class <%= class_name.demodulize %> < <%= parent_class_name %>
3
+ <% if includes_store_concern? -%>
4
+ include Spree::SingleStoreResource
5
+
6
+ <% end -%>
3
7
  <% if custom_fields? -%>
4
8
  include Spree::HasCustomFields
5
9
  include Spree::Metadata
@@ -10,7 +14,9 @@ module Spree
10
14
 
11
15
  <% end -%>
12
16
  has_prefix_id :<%= id_prefix %>
13
-
17
+ <% if lifecycle_events? -%>
18
+ publishes_lifecycle_events
19
+ <% end -%>
14
20
  <% attributes.select(&:reference?).each do |attribute| -%>
15
21
  <% if attribute.polymorphic? -%>
16
22
  belongs_to :<%= attribute.name %>, polymorphic: true
@@ -38,10 +44,10 @@ module Spree
38
44
  has_secure_password
39
45
  <% end -%>
40
46
  <% required_attrs = attributes.reject { |a| a.reference? || a.type == :boolean || a.token? || a.attachment? || a.attachments? || a.rich_text? || a.password_digest? } -%>
41
- <% if required_attrs.any? %>
42
- <% required_attrs.each do |attr| -%>
43
- validates :<%= attr.name %>, presence: true<% if attr.has_uniq_index? %>, uniqueness: { scope: spree_base_uniqueness_scope }<% end %>
47
+ <% if required_attrs.any? -%>
44
48
 
49
+ <% required_attrs.each do |attr| -%>
50
+ validates :<%= attr.name %>, presence: true<% if attr.has_uniq_index? %>, uniqueness: { scope: [<%= ':store_id, ' if store_scoped? %>*spree_base_uniqueness_scope] }<% end %>
45
51
  <% end -%>
46
52
  <% end -%>
47
53
  <% ransackable = attributes.reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }.map(&:name) -%>
@@ -3,9 +3,9 @@
3
3
  module Spree
4
4
  # spree:subscriber — scaffold an event subscriber and register it.
5
5
  #
6
- # bin/rails g spree:subscriber OmsOrderSync order.completed order.canceled
6
+ # bin/rails g spree:subscriber OmsOrderSync order.placed order.canceled
7
7
  # bin/rails g spree:subscriber MyApp::BrandSync brand.created brand.updated
8
- # bin/rails g spree:subscriber CriticalSync order.completed --sync
8
+ # bin/rails g spree:subscriber CriticalSync order.placed --sync
9
9
  #
10
10
  # Subscribers are not auto-discovered — they must be appended to the
11
11
  # Spree.subscribers array. Forgetting that step produces a silent no-op,
@@ -64,7 +64,7 @@ module Spree
64
64
  def warn_about_missing_events
65
65
  return if events.any?
66
66
 
67
- say_status :note, "no events given — edit `subscribes_to` in the generated subscriber (e.g. 'order.completed')", :yellow
67
+ say_status :note, "no events given — edit `subscribes_to` in the generated subscriber (e.g. 'order.placed')", :yellow
68
68
  end
69
69
 
70
70
  private
@@ -40,23 +40,23 @@ module Spree
40
40
  # case seller invitations fall back to the dashboard origin.
41
41
  preference :seller_panel_url, :string, default: nil, env: 'SPREE_SELLER_PANEL_URL'
42
42
  preference :allow_checkout_on_gateway_error, :boolean, default: false, deprecated: 'Nothing reads this in Spree 6 — completion checks whether payments cover the total, so a failed gateway call never completes an order'
43
- preference :allow_empty_price_amount, :boolean, default: false
43
+ preference :allow_empty_price_amount, :boolean, default: false, env: 'SPREE_ALLOW_EMPTY_PRICE_AMOUNT'
44
44
  preference :alternative_shipping_phone, :boolean, default: false, deprecated: 'Nothing reads this in Spree 6'
45
- preference :always_include_confirm_step, :boolean, default: false # Ensures confirmation step is always in checkout_progress bar, but does not force a confirm step if your payment methods do not support it.
46
- preference :always_use_translations, :boolean, default: false
45
+ preference :always_include_confirm_step, :boolean, default: false, deprecated: 'Use the always_include_confirm_step preference in the Spree::Store model'
46
+ preference :always_use_translations, :boolean, default: false, env: 'SPREE_ALWAYS_USE_TRANSLATIONS'
47
47
  preference :auto_capture, :boolean, default: true, deprecated: 'Set it on the store instead' # automatically capture the credit card (as opposed to just authorize and capture later)
48
48
  preference :auto_capture_on_dispatch, :boolean, default: false, deprecated: 'Set it on the store instead' # Captures payment on dispatch rather than at checkout.
49
49
  preference :company, :boolean, default: false, deprecated: 'Use the company_field_enabled preference in the Spree::Store model' # Request company field for billing and shipping addr
50
- preference :credit_to_new_allocation, :boolean, default: false
51
- preference :disable_migration_check, :boolean, default: false # when turned on disables the startup warning about missing engine migrations
50
+ preference :credit_to_new_allocation, :boolean, default: false, env: 'SPREE_CREDIT_TO_NEW_ALLOCATION'
51
+ preference :disable_migration_check, :boolean, default: false, env: 'SPREE_DISABLE_MIGRATION_CHECK' # when turned on disables the startup warning about missing engine migrations
52
52
  preference :disable_sku_validation, :boolean, default: false, deprecated: 'Set it on the store instead' # when turned on disables the built-in SKU uniqueness validation
53
53
  preference :disable_store_presence_validation, :boolean, default: false, deprecated: true # when turned off disables Store presence validation for Products and Payment Methods
54
- preference :events_log_enabled, :boolean, default: true # Log all Spree events to Rails logger
54
+ preference :events_log_enabled, :boolean, default: true, env: 'SPREE_EVENTS_LOG_ENABLED' # Log all Spree events to Rails logger
55
55
  preference :expedited_exchanges, :boolean, default: false, deprecated: 'Exchanges are their own record in Spree 6 — see Spree::Exchange and the Exchanges::Fulfill workflow'
56
56
  preference :expedited_exchanges_days_window, :integer, default: 14, deprecated: 'Exchanges are their own record in Spree 6 — see Spree::Exchange and the Exchanges::Fulfill workflow'
57
- preference :geocode_addresses, :boolean, default: true
58
- preference :images_save_from_url_job_attempts, :integer, default: 5
59
- preference :max_image_download_size, :integer, default: 20_971_520 # 20 MB in bytes
57
+ preference :geocode_addresses, :boolean, default: true, env: 'SPREE_GEOCODE_ADDRESSES'
58
+ preference :images_save_from_url_job_attempts, :integer, default: 5, env: 'SPREE_IMAGES_SAVE_FROM_URL_JOB_ATTEMPTS'
59
+ preference :max_image_download_size, :integer, default: 20_971_520, env: 'SPREE_MAX_IMAGE_DOWNLOAD_SIZE' # 20 MB in bytes
60
60
 
61
61
  # Preprocessed product image variant sizes at 2x retina resolution.
62
62
  # These variants are generated on upload to reduce runtime processing.
@@ -113,12 +113,12 @@ module Spree
113
113
  end
114
114
 
115
115
  # Maximum size of an uploaded product video, in bytes.
116
- preference :max_video_upload_size, :integer, default: 524_288_000 # 500 MB
116
+ preference :max_video_upload_size, :integer, default: 524_288_000, env: 'SPREE_MAX_VIDEO_UPLOAD_SIZE' # 500 MB
117
117
 
118
118
  # Maximum size of a document a seller uploads for an onboarding
119
119
  # requirement, in bytes. Small on purpose: these are certificates and
120
120
  # registrations, and an unbounded upload is a way to fill a disk.
121
- preference :max_seller_document_upload_size, :integer, default: 20_971_520 # 20 MB
121
+ preference :max_seller_document_upload_size, :integer, default: 20_971_520, env: 'SPREE_MAX_SELLER_DOCUMENT_UPLOAD_SIZE' # 20 MB
122
122
  preference :non_expiring_credit_types, :array, default: [], deprecated: 'Nothing reads this in Spree 6 — store credits no longer carry a category, and expiry lives on Spree::GiftCard'
123
123
  preference :products_per_page, :integer, default: 12, deprecated: 'Nothing reads this in Spree 6 — pass per_page to the API instead'
124
124
  preference :restock_inventory, :boolean, default: true, deprecated: 'Restocking is decided per line item by Spree::ReturnLineItem#resellable'
@@ -132,9 +132,9 @@ module Spree
132
132
  # into failures (what this repo's API test suite runs); 'off' disables.
133
133
  preference :store_scope_guard, :string, default: 'log', env: 'SPREE_STORE_SCOPE_GUARD'
134
134
  # Tiered cart-expiry reaper (docs/plans/6.0-cart-order-split.md Decision 5)
135
- preference :guest_cart_expiry_days, :integer, default: 30
136
- preference :customer_cart_expiry_days, :integer, default: 90
137
- preference :empty_cart_expiry_hours, :integer, default: 48
135
+ preference :guest_cart_expiry_days, :integer, default: 30, env: 'SPREE_GUEST_CART_EXPIRY_DAYS'
136
+ preference :customer_cart_expiry_days, :integer, default: 90, env: 'SPREE_CUSTOMER_CART_EXPIRY_DAYS'
137
+ preference :empty_cart_expiry_hours, :integer, default: 48, env: 'SPREE_EMPTY_CART_EXPIRY_HOURS'
138
138
  preference :show_variant_full_price, :boolean, default: false, deprecated: 'Nothing reads this in Spree 6 — a storefront decides how it renders prices'
139
139
  preference :show_products_without_price, :boolean, default: false, deprecated: 'Set it on the store instead'
140
140
  preference :tax_using_ship_address, :boolean, default: true, deprecated: 'Set it on the store instead'
@@ -149,33 +149,33 @@ module Spree
149
149
  preference :storefront_pages_path, :string, default: 'pages', deprecated: 'Nothing reads this in Spree 6 — storefront routes are owned by the storefront'
150
150
 
151
151
  # coupon codes
152
- preference :coupon_codes_web_limit, :integer, default: 500 # number of coupon codes to be generated in the web process, more than this will be generated in a background job
153
- preference :coupon_codes_total_limit, :integer, default: 5000 # the maximum number of coupon codes to be generated
152
+ preference :coupon_codes_web_limit, :integer, default: 500, env: 'SPREE_COUPON_CODES_WEB_LIMIT' # number of coupon codes to be generated in the web process, more than this will be generated in a background job
153
+ preference :coupon_codes_total_limit, :integer, default: 5000, env: 'SPREE_COUPON_CODES_TOTAL_LIMIT' # the maximum number of coupon codes to be generated
154
154
 
155
155
  # password reset
156
- preference :admin_password_reset_expires_in, :integer, default: 15 # admin password reset token expiration time in minutes
157
- preference :customer_password_reset_expires_in, :integer, default: 15 # password reset token expiration time in minutes
156
+ preference :admin_password_reset_expires_in, :integer, default: 15, env: 'SPREE_ADMIN_PASSWORD_RESET_EXPIRES_IN' # admin password reset token expiration time in minutes
157
+ preference :customer_password_reset_expires_in, :integer, default: 15, env: 'SPREE_CUSTOMER_PASSWORD_RESET_EXPIRES_IN' # password reset token expiration time in minutes
158
158
 
159
159
  # account lockout
160
- preference :max_failed_login_attempts, :integer, default: 5 # failed login attempts before an account is locked
161
- preference :lockout_duration, :integer, default: 1800 # lockout duration in seconds (30 minutes)
160
+ preference :max_failed_login_attempts, :integer, default: 5, env: 'SPREE_MAX_FAILED_LOGIN_ATTEMPTS' # failed login attempts before an account is locked
161
+ preference :lockout_duration, :integer, default: 1800, env: 'SPREE_LOCKOUT_DURATION' # lockout duration in seconds (30 minutes)
162
162
 
163
163
  # password policy
164
164
  # NIST 800-63B recommends a length floor with no composition rules (no forced
165
165
  # symbols/digits, which push users toward predictable substitutions).
166
- preference :minimum_password_length, :integer, default: 8
166
+ preference :minimum_password_length, :integer, default: 8, env: 'SPREE_MINIMUM_PASSWORD_LENGTH'
167
167
  # bcrypt silently truncates past 72 bytes — without a cap a long passphrase and
168
168
  # its 72-byte prefix are the same password. A correctness guard, not policy.
169
- preference :maximum_password_length, :integer, default: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
169
+ preference :maximum_password_length, :integer, default: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED, env: 'SPREE_MAXIMUM_PASSWORD_LENGTH'
170
170
  # To replace the policy itself, assign Spree.password_validator — a class,
171
171
  # not a preference.
172
172
 
173
173
  # gift cards
174
- preference :gift_card_batch_web_limit, :integer, default: 500 # number of gift card codes to be generated in the web process, more than this will be generated in a background job
175
- preference :gift_card_batch_limit, :integer, default: 50_000
174
+ preference :gift_card_batch_web_limit, :integer, default: 500, env: 'SPREE_GIFT_CARD_BATCH_WEB_LIMIT' # number of gift card codes to be generated in the web process, more than this will be generated in a background job
175
+ preference :gift_card_batch_limit, :integer, default: 50_000, env: 'SPREE_GIFT_CARD_BATCH_LIMIT'
176
176
 
177
177
  # imports
178
- preference :large_import_threshold, :integer, default: 500 # imports with more rows than this skip per-row UI broadcasts and use bulk processing
178
+ preference :large_import_threshold, :integer, default: 500, env: 'SPREE_LARGE_IMPORT_THRESHOLD' # imports with more rows than this skip per-row UI broadcasts and use bulk processing
179
179
 
180
180
  end
181
181
  end
@@ -45,6 +45,7 @@ module Spree
45
45
  :reports,
46
46
  :translatable_resources,
47
47
  :taggable_types,
48
+ :actor_classes,
48
49
  :custom_fields,
49
50
  :reporting,
50
51
  :analytics_events,
@@ -88,6 +89,12 @@ module Spree
88
89
  Spree::Deprecation = ActiveSupport::Deprecation.new('6.0', 'Spree')
89
90
  end
90
91
 
92
+ # Runs after initializers so an explicitly assigned preference — which
93
+ # wins over the environment — is never rejected for a stale env var.
94
+ config.after_initialize do
95
+ Spree::Core::Configuration.validate_env!(Spree::Config)
96
+ end
97
+
91
98
  # I18n's config lives in fiber/thread-local storage that survives across
92
99
  # requests on reused server threads, so a request that never assigns its
93
100
  # own locale would render in whatever locale the previous request on the
@@ -105,6 +112,13 @@ module Spree
105
112
  app.config.spree.subscribers = []
106
113
  end
107
114
 
115
+ # Seeded before application initializers so an extension registering an
116
+ # actor class has something to append to. The defaults are unioned in
117
+ # after initialization, where Spree.admin_user_class is finally known.
118
+ initializer 'spree.register.actor_classes', before: :load_config_initializers do |app|
119
+ app.config.spree.actor_classes = []
120
+ end
121
+
108
122
  initializer 'spree.register.calculators', before: :after_initialize do |app|
109
123
  end
110
124
 
@@ -121,6 +135,14 @@ module Spree
121
135
  app.config.spree.line_item_comparison_hooks = Set.new
122
136
  end
123
137
 
138
+ # The one eligibility rule core ships: a per-market return window,
139
+ # bypassed by staff. Registered before application initializers so a
140
+ # store can unregister it in its own initializer.
141
+ initializer 'spree.returns.register_eligibility_validator', before: :load_config_initializers do
142
+ Spree.hooks.register('returns.create.validate', 'Spree::Returns::EligibilityValidator')
143
+ Spree.hooks.register('exchanges.create.validate', 'Spree::Returns::EligibilityValidator')
144
+ end
145
+
124
146
  initializer 'spree.register.payment_methods', after: 'acts_as_list.insert_into_active_record' do |app|
125
147
  end
126
148
 
@@ -525,6 +547,19 @@ module Spree
525
547
  Spree.customer_class.to_s
526
548
  ]
527
549
 
550
+ # Models that may be recorded as having performed an action — the
551
+ # vocabulary an `acted_by` association's `*_type` column is validated
552
+ # against. Extend in an app initializer to register an App or bot
553
+ # class, which must include Spree::Actor:
554
+ # Rails.application.config.spree.actor_classes << 'MyApp::App'.
555
+ #
556
+ # Unioned rather than assigned, so what an initializer registered
557
+ # above survives.
558
+ Rails.application.config.spree.actor_classes |= [
559
+ Spree.admin_user_class.to_s,
560
+ 'Spree::ApiKey'
561
+ ]
562
+
528
563
  Rails.application.config.spree.custom_fields.types = [
529
564
  Spree::CustomFields::ShortText,
530
565
  Spree::CustomFields::LongText,
@@ -664,14 +699,6 @@ module Spree
664
699
  end
665
700
  end
666
701
 
667
- # The one eligibility rule core ships: a per-market return window,
668
- # bypassed by staff. Registered after application initializers so a
669
- # store can unregister it in its own initializer.
670
- initializer 'spree.returns.register_eligibility_validator', after: :load_config_initializers do
671
- Spree.hooks.register('returns.create.validate', 'Spree::Returns::EligibilityValidator')
672
- Spree.hooks.register('exchanges.create.validate', 'Spree::Returns::EligibilityValidator')
673
- end
674
-
675
702
  # A hook registered against a key no workflow declares would never fire
676
703
  # and never say so. Checking after eager load turns that typo into a
677
704
  # boot failure. Skipped when eager loading is off (development,