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.
- checksums.yaml +4 -4
- data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
- data/app/jobs/spree/sample_data/load_job.rb +7 -2
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
- data/app/mailers/spree/admin_user_mailer.rb +2 -2
- data/app/models/concerns/spree/customer_methods.rb +17 -1
- data/app/models/concerns/spree/has_number.rb +3 -2
- data/app/models/concerns/spree/prefixed_id.rb +4 -1
- data/app/models/concerns/spree/purchase/totals.rb +3 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
- data/app/models/concerns/spree/translatable_resource.rb +6 -7
- data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
- data/app/models/spree/adjusters/promotion.rb +1 -1
- data/app/models/spree/authentication/lockout.rb +34 -0
- data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
- data/app/models/spree/claim.rb +1 -0
- data/app/models/spree/commission_line.rb +4 -2
- data/app/models/spree/commission_rule.rb +1 -1
- data/app/models/spree/coupon_code.rb +14 -2
- data/app/models/spree/current.rb +15 -0
- data/app/models/spree/exchange.rb +1 -0
- data/app/models/spree/export.rb +8 -5
- data/app/models/spree/exports/customers.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +127 -0
- data/app/models/spree/gift_card.rb +9 -0
- data/app/models/spree/invitation.rb +18 -5
- data/app/models/spree/number_generators/sequential.rb +2 -2
- data/app/models/spree/option_value.rb +7 -0
- data/app/models/spree/order.rb +35 -12
- data/app/models/spree/order_group.rb +78 -6
- data/app/models/spree/payment_source.rb +1 -1
- data/app/models/spree/payment_split.rb +24 -0
- data/app/models/spree/price.rb +74 -0
- data/app/models/spree/product.rb +4 -0
- data/app/models/spree/product_option_type.rb +11 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
- data/app/models/spree/promotion/rules/category.rb +14 -9
- data/app/models/spree/promotion/rules/product.rb +17 -1
- data/app/models/spree/promotion.rb +1 -1
- data/app/models/spree/promotion_action.rb +12 -1
- data/app/models/spree/promotion_handler/coupon.rb +50 -5
- data/app/models/spree/promotion_rule.rb +14 -0
- data/app/models/spree/return.rb +16 -6
- data/app/models/spree/return_line_item.rb +19 -0
- data/app/models/spree/search_provider/database.rb +10 -2
- data/app/models/spree/seller_payout.rb +3 -1
- data/app/models/spree/seller_transfer.rb +3 -1
- data/app/models/spree/store_credit.rb +4 -0
- data/app/models/spree/translations/batch.rb +4 -2
- data/app/models/spree/user_identity.rb +10 -6
- data/app/models/spree/variant.rb +8 -3
- data/app/models/spree/webhook_delivery.rb +28 -0
- data/app/models/spree/webhook_endpoint.rb +14 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +22 -12
- data/app/services/spree/companies/add_member.rb +8 -2
- data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
- data/app/services/spree/order_groups/allocate_payments.rb +3 -0
- data/app/services/spree/orders/build_fulfillments.rb +6 -0
- data/app/services/spree/orders/update_statuses.rb +8 -2
- data/app/services/spree/prices/bulk_upsert.rb +186 -47
- data/app/services/spree/sample_data/import_builder.rb +1 -1
- data/app/services/spree/sample_data/loader.rb +54 -40
- data/app/services/spree/seeds/all.rb +1 -26
- data/app/services/spree/seeds/allowed_origins.rb +5 -4
- data/app/services/spree/seeds/api_keys.rb +10 -9
- data/app/services/spree/seeds/channels.rb +9 -8
- data/app/services/spree/seeds/commission_rates.rb +21 -20
- data/app/services/spree/seeds/customer_groups.rb +5 -4
- data/app/services/spree/seeds/digital_delivery.rb +7 -8
- data/app/services/spree/seeds/payment_methods.rb +12 -11
- data/app/services/spree/seeds/product_types.rb +7 -8
- data/app/services/spree/seeds/returns_environment.rb +12 -11
- data/app/services/spree/seeds/roles.rb +8 -7
- data/app/services/spree/seeds/saved_reports.rb +19 -20
- data/app/services/spree/seeds/seller_requirements.rb +5 -4
- data/app/services/spree/seeds/store_resources.rb +41 -0
- data/app/services/spree/seeds/store_scoped.rb +15 -0
- data/app/services/spree/seeds/tax_categories.rb +10 -9
- data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
- data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
- data/app/subscribers/spree/order_status_subscriber.rb +15 -7
- data/app/workflows/spree/carts/add_item.rb +5 -7
- data/app/workflows/spree/carts/complete.rb +20 -64
- data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
- data/app/workflows/spree/claims/create.rb +31 -1
- data/app/workflows/spree/claims/resolve.rb +7 -11
- data/app/workflows/spree/exchanges/create.rb +11 -0
- data/app/workflows/spree/exchanges/fulfill.rb +21 -13
- data/app/workflows/spree/orders/complete.rb +99 -2
- data/app/workflows/spree/price_lists/update.rb +17 -0
- data/app/workflows/spree/refunds/order_payments.rb +55 -1
- data/app/workflows/spree/returns/create.rb +9 -15
- data/app/workflows/spree/returns/refund.rb +15 -24
- data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
- data/app/workflows/spree/sellers/invite.rb +1 -1
- data/config/locales/en.yml +7 -119
- data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
- data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
- data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
- data/db/sample_data/channels.rb +1 -1
- data/db/sample_data/collections.rb +1 -1
- data/db/sample_data/custom_field_definitions.rb +1 -1
- data/db/sample_data/digital_products.rb +1 -1
- data/db/sample_data/fulfillment.rb +1 -1
- data/db/sample_data/options.rb +4 -2
- data/db/sample_data/orders.rb +9 -10
- data/db/sample_data/payment_methods.rb +18 -18
- data/db/sample_data/product_type_categories.rb +1 -1
- data/db/sample_data/product_types.rb +2 -2
- data/db/sample_data/promotions.rb +2 -4
- data/db/sample_data/wholesale.rb +1 -1
- data/lib/generators/spree/api_resource/api_resource_generator.rb +58 -1
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +6 -5
- data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
- data/lib/spree/core/engine.rb +8 -8
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/iso_data.rb +23 -4
- data/lib/spree/store_scope_guard.rb +16 -1
- data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
- data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
- data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/sample_data.rake +8 -2
- data/lib/tasks/upgrade.rake +6 -21
- metadata +13 -4
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
Spree::
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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::
|
|
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::
|
|
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::
|
|
1
|
+
promotion = Spree::Current.store.promotions.where(
|
|
2
2
|
name: 'Free Shipping',
|
|
3
3
|
code: 'FREESHIP'
|
|
4
|
-
).first_or_create!
|
|
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!
|
data/db/sample_data/wholesale.rb
CHANGED
|
@@ -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::
|
|
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
|
-
|
|
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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
22
|
-
|
|
22
|
+
def resource_permitted_attributes
|
|
23
|
+
[<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -20,8 +20,8 @@ module Spree
|
|
|
20
20
|
<% end -%>
|
|
21
21
|
<% if writable? -%>
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
data/lib/spree/core/engine.rb
CHANGED
|
@@ -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,
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/iso_data.rb
CHANGED
|
@@ -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[
|
|
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
|
-
|
|
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).
|
|
@@ -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
|
-
|
|
33
|
-
|
|
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"
|
data/lib/tasks/sample_data.rake
CHANGED
|
@@ -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
|
-
|
|
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
|
|
data/lib/tasks/upgrade.rake
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
102
|
-
#
|
|
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|
|
|
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 #{
|
|
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)
|