spree_core 6.0.0.beta2 → 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 (132) 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 +7 -2
  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/customer_methods.rb +17 -1
  7. data/app/models/concerns/spree/has_number.rb +3 -2
  8. data/app/models/concerns/spree/prefixed_id.rb +4 -1
  9. data/app/models/concerns/spree/purchase/totals.rb +3 -1
  10. data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
  11. data/app/models/concerns/spree/translatable_resource.rb +6 -7
  12. data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
  13. data/app/models/spree/adjusters/promotion.rb +1 -1
  14. data/app/models/spree/authentication/lockout.rb +34 -0
  15. data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
  16. data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
  17. data/app/models/spree/claim.rb +1 -0
  18. data/app/models/spree/commission_line.rb +4 -2
  19. data/app/models/spree/commission_rule.rb +1 -1
  20. data/app/models/spree/coupon_code.rb +14 -2
  21. data/app/models/spree/current.rb +15 -0
  22. data/app/models/spree/exchange.rb +1 -0
  23. data/app/models/spree/export.rb +8 -5
  24. data/app/models/spree/exports/customers.rb +2 -2
  25. data/app/models/spree/fulfillment_group.rb +127 -0
  26. data/app/models/spree/gift_card.rb +9 -0
  27. data/app/models/spree/invitation.rb +18 -5
  28. data/app/models/spree/number_generators/sequential.rb +2 -2
  29. data/app/models/spree/option_value.rb +7 -0
  30. data/app/models/spree/order.rb +35 -12
  31. data/app/models/spree/order_group.rb +78 -6
  32. data/app/models/spree/payment_source.rb +1 -1
  33. data/app/models/spree/payment_split.rb +24 -0
  34. data/app/models/spree/price.rb +74 -0
  35. data/app/models/spree/product.rb +4 -0
  36. data/app/models/spree/product_option_type.rb +11 -0
  37. data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
  38. data/app/models/spree/promotion/rules/category.rb +14 -9
  39. data/app/models/spree/promotion/rules/product.rb +17 -1
  40. data/app/models/spree/promotion.rb +1 -1
  41. data/app/models/spree/promotion_action.rb +12 -1
  42. data/app/models/spree/promotion_handler/coupon.rb +50 -5
  43. data/app/models/spree/promotion_rule.rb +14 -0
  44. data/app/models/spree/return.rb +16 -6
  45. data/app/models/spree/return_line_item.rb +19 -0
  46. data/app/models/spree/search_provider/database.rb +10 -2
  47. data/app/models/spree/seller_payout.rb +3 -1
  48. data/app/models/spree/seller_transfer.rb +3 -1
  49. data/app/models/spree/store_credit.rb +4 -0
  50. data/app/models/spree/translations/batch.rb +4 -2
  51. data/app/models/spree/user_identity.rb +10 -6
  52. data/app/models/spree/variant.rb +8 -3
  53. data/app/models/spree/webhook_delivery.rb +28 -0
  54. data/app/models/spree/webhook_endpoint.rb +14 -0
  55. data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
  56. data/app/services/spree/carts/split_by_seller.rb +22 -12
  57. data/app/services/spree/companies/add_member.rb +8 -2
  58. data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
  59. data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
  60. data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
  61. data/app/services/spree/order_groups/allocate_payments.rb +3 -0
  62. data/app/services/spree/orders/build_fulfillments.rb +6 -0
  63. data/app/services/spree/orders/update_statuses.rb +8 -2
  64. data/app/services/spree/prices/bulk_upsert.rb +186 -47
  65. data/app/services/spree/sample_data/import_builder.rb +1 -1
  66. data/app/services/spree/sample_data/loader.rb +54 -40
  67. data/app/services/spree/seeds/all.rb +1 -26
  68. data/app/services/spree/seeds/allowed_origins.rb +5 -4
  69. data/app/services/spree/seeds/api_keys.rb +10 -9
  70. data/app/services/spree/seeds/channels.rb +9 -8
  71. data/app/services/spree/seeds/commission_rates.rb +21 -20
  72. data/app/services/spree/seeds/customer_groups.rb +5 -4
  73. data/app/services/spree/seeds/digital_delivery.rb +7 -8
  74. data/app/services/spree/seeds/payment_methods.rb +12 -11
  75. data/app/services/spree/seeds/product_types.rb +7 -8
  76. data/app/services/spree/seeds/returns_environment.rb +12 -11
  77. data/app/services/spree/seeds/roles.rb +8 -7
  78. data/app/services/spree/seeds/saved_reports.rb +19 -20
  79. data/app/services/spree/seeds/seller_requirements.rb +5 -4
  80. data/app/services/spree/seeds/store_resources.rb +41 -0
  81. data/app/services/spree/seeds/store_scoped.rb +15 -0
  82. data/app/services/spree/seeds/tax_categories.rb +10 -9
  83. data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
  84. data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
  85. data/app/subscribers/spree/order_status_subscriber.rb +15 -7
  86. data/app/workflows/spree/carts/add_item.rb +5 -7
  87. data/app/workflows/spree/carts/complete.rb +20 -64
  88. data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
  89. data/app/workflows/spree/claims/create.rb +31 -1
  90. data/app/workflows/spree/claims/resolve.rb +7 -11
  91. data/app/workflows/spree/exchanges/create.rb +11 -0
  92. data/app/workflows/spree/exchanges/fulfill.rb +21 -13
  93. data/app/workflows/spree/orders/complete.rb +99 -2
  94. data/app/workflows/spree/price_lists/update.rb +17 -0
  95. data/app/workflows/spree/refunds/order_payments.rb +55 -1
  96. data/app/workflows/spree/returns/create.rb +9 -15
  97. data/app/workflows/spree/returns/refund.rb +15 -24
  98. data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
  99. data/app/workflows/spree/sellers/invite.rb +1 -1
  100. data/config/locales/en.yml +7 -119
  101. data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
  102. data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
  103. data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
  104. data/db/sample_data/channels.rb +1 -1
  105. data/db/sample_data/collections.rb +1 -1
  106. data/db/sample_data/custom_field_definitions.rb +1 -1
  107. data/db/sample_data/digital_products.rb +1 -1
  108. data/db/sample_data/fulfillment.rb +1 -1
  109. data/db/sample_data/options.rb +4 -2
  110. data/db/sample_data/orders.rb +9 -10
  111. data/db/sample_data/payment_methods.rb +18 -18
  112. data/db/sample_data/product_type_categories.rb +1 -1
  113. data/db/sample_data/product_types.rb +2 -2
  114. data/db/sample_data/promotions.rb +2 -4
  115. data/db/sample_data/wholesale.rb +1 -1
  116. data/lib/generators/spree/api_resource/api_resource_generator.rb +58 -1
  117. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +6 -5
  118. data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
  119. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
  120. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  121. data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
  122. data/lib/spree/core/engine.rb +8 -8
  123. data/lib/spree/core/version.rb +1 -1
  124. data/lib/spree/iso_data.rb +23 -4
  125. data/lib/spree/store_scope_guard.rb +16 -1
  126. data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
  127. data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
  128. data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
  129. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
  130. data/lib/tasks/sample_data.rake +8 -2
  131. data/lib/tasks/upgrade.rake +6 -21
  132. metadata +13 -4
@@ -1,19 +1,19 @@
1
- Spree::Store.all.find_each do |store|
2
- cc_payment_method = store.payment_methods.find_or_initialize_by(
3
- type: 'Spree::Gateway::Bogus',
4
- name: 'Credit Card',
5
- description: 'Bogus payment gateway.',
6
- active: true
7
- )
8
- cc_payment_method.storefront_visible = false
9
- cc_payment_method.save!
1
+ store = Spree::Current.store
10
2
 
11
- check_payment_method = store.payment_methods.find_or_initialize_by(
12
- type: 'Spree::PaymentMethod::Check',
13
- name: 'Check',
14
- description: 'Pay by check.',
15
- active: true
16
- )
17
- check_payment_method.storefront_visible = false
18
- check_payment_method.save!
19
- end
3
+ cc_payment_method = store.payment_methods.find_or_initialize_by(
4
+ type: 'Spree::Gateway::Bogus',
5
+ name: 'Credit Card',
6
+ description: 'Bogus payment gateway.',
7
+ active: true
8
+ )
9
+ cc_payment_method.storefront_visible = false
10
+ cc_payment_method.save!
11
+
12
+ check_payment_method = store.payment_methods.find_or_initialize_by(
13
+ type: 'Spree::PaymentMethod::Check',
14
+ name: 'Check',
15
+ description: 'Pay by check.',
16
+ active: true
17
+ )
18
+ check_payment_method.storefront_visible = false
19
+ check_payment_method.save!
@@ -5,7 +5,7 @@
5
5
  # categories — before it there is nothing to link to. Seeding is additive and
6
6
  # only applies to products created from here on, so linking after the fact
7
7
  # changes nothing about the imported catalog.
8
- store = Spree::Store.default
8
+ store = Spree::Current.store
9
9
 
10
10
  categories_by_type = {
11
11
  'Kitchen Appliance' => 'Kitchen',
@@ -10,9 +10,9 @@
10
10
  #
11
11
  # Categories are attached afterwards by product_type_categories.rb — the import
12
12
  # is what creates them, so there is nothing to link to at this point.
13
- store = Spree::Store.default
13
+ store = Spree::Current.store
14
14
 
15
- color_option_type = Spree::OptionType.find_by(name: 'color')
15
+ color_option_type = Spree::OptionType.for_store(store).find_by(name: 'color')
16
16
 
17
17
  definitions = store.custom_field_definitions.where(
18
18
  namespace: 'custom',
@@ -1,8 +1,6 @@
1
- promotion = Spree::Promotion.where(
1
+ promotion = Spree::Current.store.promotions.where(
2
2
  name: 'Free Shipping',
3
3
  code: 'FREESHIP'
4
- ).first_or_create! do |promo|
5
- promo.store = Spree::Store.default
6
- end
4
+ ).first_or_create!
7
5
 
8
6
  Spree::Promotion::Actions::FreeShipping.where(promotion: promotion).first_or_create!
@@ -2,7 +2,7 @@
2
2
  # Spree::Seeds::Channels: an approved demo buyer and a customer-group price
3
3
  # list, so a fresh install can walk the whole B2B portal story — sign in as
4
4
  # the buyer, see wholesale prices, check out on the wholesale channel.
5
- store = Spree::Store.default
5
+ store = Spree::Current.store
6
6
 
7
7
  wholesale_group = store.customer_groups.find_or_create_by!(name: Spree::Seeds::CustomerGroups::WHOLESALE_NAME)
8
8
 
@@ -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,
@@ -182,6 +188,35 @@ module Spree
182
188
  inject_route_for(:admin, admin_route_line) if options[:admin]
183
189
  end
184
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
+
185
220
  no_tasks do
186
221
  # A host app declares its API routes in config/routes.rb, inside the
187
222
  # engine's route hook that spree-starter ships ready to fill in. The
@@ -253,7 +288,11 @@ module Spree
253
288
  say ' Next steps:', :yellow
254
289
  say ' 1. Review the generated model — add validations, scopes, callbacks'
255
290
  say ' 2. Apply the migration: pnpm exec spree migrate'
256
- say ' 3. Set up authorization (CanCanCan ability) for the resource'
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
257
296
  say " 4. Add `has_many :#{plural_name}` to Spree::Store" if store_scoped?
258
297
  if options[:store] || options[:admin]
259
298
  say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree rspec spec/controllers/spree/api/v3/"
@@ -295,6 +334,13 @@ module Spree
295
334
  file_name.pluralize
296
335
  end
297
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
+
298
344
  # Attributes the controller permits on write. By default, every column
299
345
  # except references (FKs come through nested), attachments, rich text.
300
346
  def permitted_attribute_names
@@ -306,6 +352,17 @@ module Spree
306
352
 
307
353
  private
308
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
+
309
366
  # Where the model file lands. We use parent's class_path + file_name so
310
367
  # this stays in sync with whatever Spree::ModelGenerator decides.
311
368
  def model_file_destination
@@ -3,9 +3,10 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class <%= plural_name.camelize %>Controller < ResourceController
6
- # Which API key scope authorizes this endpoint — `read_<%= plural_name %>`
7
- # and `write_<%= plural_name %>`. Point it at an existing resource name
8
- # instead if this endpoint belongs under one.
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.
9
10
  scoped_resource :<%= plural_name %>
10
11
 
11
12
  protected
@@ -18,8 +19,8 @@ module Spree
18
19
  Spree::Api::V3::Admin::<%= bare_class_name %>Serializer
19
20
  end
20
21
 
21
- def permitted_params
22
- params.permit(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)
22
+ def resource_permitted_attributes
23
+ [<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
23
24
  end
24
25
  end
25
26
  end
@@ -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
@@ -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
@@ -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
@@ -135,6 +135,14 @@ module Spree
135
135
  app.config.spree.line_item_comparison_hooks = Set.new
136
136
  end
137
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
+
138
146
  initializer 'spree.register.payment_methods', after: 'acts_as_list.insert_into_active_record' do |app|
139
147
  end
140
148
 
@@ -691,14 +699,6 @@ module Spree
691
699
  end
692
700
  end
693
701
 
694
- # The one eligibility rule core ships: a per-market return window,
695
- # bypassed by staff. Registered after application initializers so a
696
- # store can unregister it in its own initializer.
697
- initializer 'spree.returns.register_eligibility_validator', after: :load_config_initializers do
698
- Spree.hooks.register('returns.create.validate', 'Spree::Returns::EligibilityValidator')
699
- Spree.hooks.register('exchanges.create.validate', 'Spree::Returns::EligibilityValidator')
700
- end
701
-
702
702
  # A hook registered against a key no workflow declares would never fire
703
703
  # and never say so. Checking after eager load turns that typo into a
704
704
  # boot failure. Skipped when eager loading is off (development,
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '6.0.0.beta2'.freeze
2
+ VERSION = '6.0.0.beta3'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -53,6 +53,13 @@ module Spree
53
53
  }
54
54
  }.freeze
55
55
 
56
+ # Some gem subdivision names are a placeholder the gem falls back to when it
57
+ # has no proper name for the entry: the value it would have translated,
58
+ # tagged with the locale it belongs to — Pakistan's "Sind (en)",
59
+ # "Baluchistan (en)". The clean name is the locale's own translation, so a
60
+ # name matching this is replaced rather than shown or matched against.
61
+ LOCALE_TAGGED_NAME = /\s\([a-z]{2}(?:-[a-z]{2})?\)\z/i
62
+
56
63
  # Retired ISO codes mapped to their current equivalent. Reading an address
57
64
  # written under the old code still finds the right subdivision; writing
58
65
  # always stores the successor.
@@ -172,16 +179,28 @@ module Spree
172
179
  # English deliberately reads the canonical +name+ rather than the +en+
173
180
  # translation: the two disagree in places (the gem translates DC as
174
181
  # "Washington DC", where the canonical name is "District of Columbia"),
175
- # and the canonical spelling is the one Spree has always stored.
182
+ # and the canonical spelling is the one Spree has always stored. The
183
+ # exception is a name the gem left as a locale-tagged placeholder
184
+ # ("Sind (en)"): that is not a spelling to preserve, so the clean English
185
+ # translation wins.
176
186
  #
177
187
  # Note the translation keys are symbols here, unlike +Country#translation+,
178
188
  # which takes a string.
179
189
  def subdivision_display_name(subdivision)
180
190
  locale = I18n.locale.to_s.downcase
181
- return subdivision.name if locale.start_with?('en')
182
-
183
191
  translations = subdivision.translations || {}
184
- translations[locale.to_sym].presence || subdivision.name
192
+ english = translations[:en].presence
193
+
194
+ if locale.start_with?('en')
195
+ name = subdivision.name
196
+ return name.match?(LOCALE_TAGGED_NAME) ? (english || name) : name
197
+ end
198
+
199
+ translated = translations[locale.to_sym].presence
200
+ return translated if translated
201
+
202
+ name = subdivision.name
203
+ name.match?(LOCALE_TAGGED_NAME) ? (english || name) : name
185
204
  end
186
205
  end
187
206
  end
@@ -72,6 +72,19 @@ module Spree
72
72
  RequestStore.store[ACTIVE_KEY] = previous
73
73
  end
74
74
 
75
+ # Opens the window for whatever declared a store context — a job, a
76
+ # webhook, a console script — and leaves it open until Spree::Current
77
+ # resets, which Rails does per request and per job. Called from
78
+ # `Spree::Current#store=`; work that never declares a store stays
79
+ # unguarded, since reading the default store through the fallback is
80
+ # not a scoping claim.
81
+ def arm!
82
+ return unless enabled?
83
+
84
+ install!
85
+ Spree::Current.store_scope_guard_armed = true
86
+ end
87
+
75
88
  # Suppresses the guard for a deliberately store-less lookup.
76
89
  def skip
77
90
  previous = RequestStore.store[SKIP_KEY]
@@ -82,7 +95,9 @@ module Spree
82
95
  end
83
96
 
84
97
  def active?
85
- RequestStore.store[ACTIVE_KEY] && !RequestStore.store[SKIP_KEY]
98
+ return false if RequestStore.store[SKIP_KEY]
99
+
100
+ RequestStore.store[ACTIVE_KEY] || Spree::Current.store_scope_guard_armed.present?
86
101
  end
87
102
 
88
103
  # Forgets memoized environment/schema state (specs, migrations).
@@ -2,5 +2,6 @@ FactoryBot.define do
2
2
  factory :invitation, class: Spree::Invitation do
3
3
  email { FFaker::Internet.email }
4
4
  association :inviter, factory: :admin_user
5
+ role { (resource || Spree::Store.current).default_user_role }
5
6
  end
6
7
  end
@@ -5,6 +5,55 @@ FactoryBot.define do
5
5
  currency { 'USD' }
6
6
  email { customer&.email }
7
7
 
8
+ # A split checkout as it actually leaves completion: each child holding a
9
+ # seller's item and the parcel that ships it. `:with_orders` builds bare
10
+ # orders, which is enough for roll-up arithmetic and not enough for
11
+ # anything that renders the purchase.
12
+ #
13
+ # One stock location per seller by default, so each parcel is quoted on its
14
+ # own delivery method and the group ships in as many parcels as it has
15
+ # children. Pass `shared_stock_location: true` for the marketplace-fulfilled
16
+ # shape instead: every child ships out of one warehouse on one method, which
17
+ # is what a divided parcel looks like after the split clones it.
18
+ trait :with_parcels do
19
+ transient do
20
+ sellers_count { 2 }
21
+ shared_stock_location { false }
22
+ end
23
+
24
+ after(:create) do |group, evaluator|
25
+ shared_location = create(:stock_location, name: "Shared #{group.number}") if evaluator.shared_stock_location
26
+ shared_method = create(:delivery_method, name: "Shared #{group.number}") if evaluator.shared_stock_location
27
+
28
+ evaluator.sellers_count.times do |index|
29
+ seller = create(:seller, :approved, store: group.store)
30
+ order = create(
31
+ :order_with_line_items,
32
+ store: group.store,
33
+ order_group: group,
34
+ seller: seller,
35
+ number: "#{group.number}-#{index + 1}",
36
+ customer: group.customer,
37
+ currency: group.currency,
38
+ line_items_count: 1
39
+ )
40
+ order.line_items.each { |line_item| line_item.update_columns(seller_id: seller.id) }
41
+
42
+ # The order factory already built the parcel; this decides where it
43
+ # ships from and by what, which is what the grouping reads.
44
+ fulfillment = order.fulfillments.first
45
+ fulfillment.update_columns(
46
+ stock_location_id: (shared_location || create(:stock_location, name: "#{seller.name} warehouse")).id
47
+ )
48
+ fulfillment.delivery_rates.destroy_all
49
+ fulfillment.add_delivery_method(shared_method || create(:delivery_method, name: "#{seller.name} delivery"), true)
50
+ order.recalculate_totals!
51
+ end
52
+
53
+ group.orders.reload
54
+ end
55
+ end
56
+
8
57
  trait :with_orders do
9
58
  transient do
10
59
  sellers_count { 2 }
@@ -29,7 +29,12 @@ FactoryBot.define do
29
29
  end
30
30
 
31
31
  factory :store_credit_payment, class: Spree::Payment, parent: :payment do
32
- payment_method { create(:store_credit_payment_method, store: (order || cart).store) }
33
- source { create(:store_credit, store: (order || cart).store, customer: (order || cart).customer) }
32
+ # An order group owns payments like a cart or an order does, and a split
33
+ # checkout paid with store credit is exactly where that matters.
34
+ payment_method { create(:store_credit_payment_method, store: (order || cart || order_group).store) }
35
+ source do
36
+ owner = order || cart || order_group
37
+ create(:store_credit, store: owner.store, customer: owner.customer)
38
+ end
34
39
  end
35
40
  end
@@ -438,6 +438,22 @@ steps:
438
438
  once several stores hold tax configuration. The NOT NULL constraints land
439
439
  in 6.1. Idempotent.
440
440
 
441
+ - id: backfill_order_coupon_codes
442
+ name: "Backfill coupon codes on placed orders"
443
+ task: "spree:backfill_order_coupon_codes"
444
+ notes: |
445
+ spree_orders gains a coupon_code column in Spree 6, matching carts.
446
+ Historical placed orders applied coupons only through the promotion
447
+ join tables, so this fills the column from the order's coupon-code
448
+ record, or failing that from the code of an applied single-code
449
+ promotion, so admin filtering and the serializer answer consistently
450
+ for old orders. Reads promotion records only — no dependency on the
451
+ adjustments conversion — and touches completed orders only, so it is
452
+ independent of the cart conversion below. Tune the load size with
453
+ BATCH_SIZE (default 500).
454
+
455
+ Idempotent — only orders whose coupon_code is still blank are scanned.
456
+
441
457
  - id: backfill_order_markets
442
458
  name: "Backfill order markets"
443
459
  task: "spree:backfill_order_markets"
@@ -664,6 +680,24 @@ steps:
664
680
  stop the run. Purge the soft-deleted rows later with
665
681
  spree:upgrade:purge_migrated_external_receives.
666
682
 
683
+ - id: backfill_actor_types
684
+ name: "Name the kind of actor on past orders, returns, refunds and receipts"
685
+ task: "spree:upgrade:backfill_actor_types"
686
+ notes: |
687
+ Orders, refunds, returns, exchanges, claims and stock receipts remember
688
+ who cancelled, approved, created or received them. In 6.0 that can be a
689
+ member of staff or an API key, so each actor id column gained a type
690
+ column beside it. Rows written before the upgrade carry an id and no
691
+ type — they could only ever have pointed at an admin user — so this
692
+ fills in the admin user class. Until it runs those rows still read as
693
+ the staff member they always were, with a deprecation warning.
694
+
695
+ Run AFTER migrate_returns and migrate_external_receives_to_purchase_orders
696
+ so any actor rows those steps write are covered too. Walks
697
+ every model that declares acted_by, so extensions are included.
698
+
699
+ Idempotent — a row that already carries a type is skipped.
700
+
667
701
  - id: recount_stock_levels
668
702
  name: "Fill in the reserved and incoming counters on every stock level"
669
703
  task: "spree:stock:recount_levels"
@@ -1,7 +1,13 @@
1
1
  namespace :spree do
2
- desc 'Loads sample data (products, customers, orders, configuration)'
2
+ desc 'Loads sample data (products, customers, orders, configuration). Set STORE_ID (prefixed or numeric) or STORE_CODE to target a store other than the default one'
3
3
  task load_sample_data: :environment do
4
- Spree::SampleData::Loader.call
4
+ store = if ENV['STORE_ID'].present?
5
+ Spree::Store.find_by_param!(ENV['STORE_ID'])
6
+ elsif ENV['STORE_CODE'].present?
7
+ Spree::Store.find_by!(code: ENV['STORE_CODE'])
8
+ end
9
+
10
+ Spree::SampleData::Loader.call(store: store)
5
11
  end
6
12
  end
7
13
 
@@ -13,7 +13,7 @@
13
13
  #
14
14
  # Usage:
15
15
  # bundle exec rake spree:upgrade # walk all eligible manifests
16
- # bundle exec rake spree:upgrade DRY_RUN=1 # print plan, run nothing
16
+ # bundle exec rake spree:upgrade DRY_RUN=1 # print the steps a run would execute, run nothing
17
17
  # bundle exec rake spree:upgrade STEP=channels # run one step by id (any manifest)
18
18
  # bundle exec rake spree:upgrade TO=5.4 # cap the walk at this version
19
19
  #
@@ -72,10 +72,9 @@ module Spree
72
72
  # rendering, invocation) stay separable and the plan can be inspected
73
73
  # without execution.
74
74
  class Runner
75
- attr_reader :target_version, :step_id, :dry_run, :target_explicit
75
+ attr_reader :target_version, :step_id, :dry_run
76
76
 
77
77
  def initialize(target_version: nil, step_id: nil, dry_run: false)
78
- @target_explicit = !target_version.nil?
79
78
  @target_version = target_version || Spree::Upgrade.installed_minor_version
80
79
  @step_id = step_id
81
80
  @dry_run = dry_run
@@ -98,36 +97,22 @@ module Spree
98
97
 
99
98
  private
100
99
 
101
- # In plan-mode without explicit TO, show manifests ahead of the installed
102
- # version — the path the operator is about to walk. Otherwise filter to
103
- # manifests whose `to` is ≤ target.
100
+ # Plan mode selects exactly what a real run executes, so a dry run after
101
+ # bumping the gem lists every backfill the upgrade is about to perform.
104
102
  def eligible_manifests
105
103
  Spree::Upgrade.available_manifests
106
- .select { |manifest| manifest_eligible?(manifest) }
104
+ .select { |manifest| Spree::Upgrade.compare(manifest[:to], target_version) <= 0 }
107
105
  .map { |entry| load_manifest_yaml(entry) }
108
106
  end
109
107
 
110
- def manifest_eligible?(manifest)
111
- if dry_run && !target_explicit
112
- Spree::Upgrade.compare(manifest[:from], Spree::Upgrade.installed_minor_version) >= 0
113
- else
114
- Spree::Upgrade.compare(manifest[:to], target_version) <= 0
115
- end
116
- end
117
-
118
108
  def load_manifest_yaml(entry)
119
109
  YAML.safe_load_file(File.join(entry[:dir], 'manifest.yml'))
120
110
  end
121
111
 
122
112
  def run_full_walk(manifests)
123
113
  total_steps = manifests.sum { |m| m['steps'].size }
124
- reported_target = if dry_run && !target_explicit
125
- manifests.map { |m| m['to'] }.compact.max_by { |v| Spree::Upgrade.version_parts(v) } || target_version
126
- else
127
- target_version
128
- end
129
114
  puts
130
- puts " Walking #{manifests.size} manifest(s), #{total_steps} step(s) total. Target: Spree #{reported_target}."
115
+ puts " Walking #{manifests.size} manifest(s), #{total_steps} step(s) total. Target: Spree #{target_version}."
131
116
 
132
117
  manifests.each do |manifest|
133
118
  print_manifest_header(manifest)