spree_core 5.4.2 → 5.5.0.rc1
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/helpers/spree/base_helper.rb +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +3 -2
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/publishable.rb +1 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +37 -14
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/event.rb +6 -6
- data/app/models/spree/export.rb +32 -5
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +6 -1
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +7 -2
- data/app/models/spree/market.rb +57 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_inventory.rb +24 -2
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/shipment.rb +10 -4
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/subscriber.rb +12 -12
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/credit_cards/destroy.rb +1 -1
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/payments/handle_webhook.rb +3 -10
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/event_log_subscriber.rb +1 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +35 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +1 -1
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +86 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -15,6 +15,9 @@ module Spree
|
|
|
15
15
|
puts 'Loading sample markets...'
|
|
16
16
|
load_ruby_file('markets')
|
|
17
17
|
|
|
18
|
+
puts 'Loading sample channels...'
|
|
19
|
+
load_ruby_file('channels')
|
|
20
|
+
|
|
18
21
|
puts 'Loading sample metafield definitions...'
|
|
19
22
|
load_ruby_file('metafield_definitions')
|
|
20
23
|
|
|
@@ -24,6 +27,12 @@ module Spree
|
|
|
24
27
|
puts 'Loading sample products...'
|
|
25
28
|
load_products
|
|
26
29
|
|
|
30
|
+
puts 'Publishing sample products on the default channel...'
|
|
31
|
+
publish_sample_products
|
|
32
|
+
|
|
33
|
+
puts 'Loading sample categories...'
|
|
34
|
+
load_categories
|
|
35
|
+
|
|
27
36
|
puts 'Loading sample product translations...'
|
|
28
37
|
load_product_translations
|
|
29
38
|
|
|
@@ -67,6 +76,27 @@ module Spree
|
|
|
67
76
|
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
|
|
68
77
|
end
|
|
69
78
|
|
|
79
|
+
def publish_sample_products
|
|
80
|
+
store = Spree::Store.default
|
|
81
|
+
store.default_channel.add_products(store.product_ids)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def load_categories
|
|
85
|
+
store = Spree::Store.default
|
|
86
|
+
csv_path = sample_data_path.join('products.csv')
|
|
87
|
+
|
|
88
|
+
require 'csv'
|
|
89
|
+
::CSV.foreach(csv_path, headers: true) do |row|
|
|
90
|
+
product = store.products.find_by(slug: row['slug'])
|
|
91
|
+
next unless product
|
|
92
|
+
|
|
93
|
+
categories = [row['category1'], row['category2'], row['category3']].compact_blank
|
|
94
|
+
next if categories.empty?
|
|
95
|
+
|
|
96
|
+
Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, categories)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
70
100
|
def load_product_translations
|
|
71
101
|
csv_path = sample_data_path.join('product_translations.csv')
|
|
72
102
|
return unless csv_path.exist?
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module StockReservations
|
|
3
|
+
class Extend
|
|
4
|
+
prepend Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:)
|
|
7
|
+
return success(order) unless Spree::Config[:stock_reservations_enabled]
|
|
8
|
+
|
|
9
|
+
expires_at = Time.current + Spree::StockReservation.ttl_for(order)
|
|
10
|
+
|
|
11
|
+
Spree::StockReservation
|
|
12
|
+
.where(order_id: order.id)
|
|
13
|
+
.update_all(expires_at: expires_at, updated_at: Time.current)
|
|
14
|
+
|
|
15
|
+
success(order)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module StockReservations
|
|
3
|
+
class Reserve
|
|
4
|
+
prepend Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:)
|
|
7
|
+
return success(order) unless Spree::Config[:stock_reservations_enabled]
|
|
8
|
+
|
|
9
|
+
expires_at = Time.current + Spree::StockReservation.ttl_for(order)
|
|
10
|
+
|
|
11
|
+
ApplicationRecord.transaction do
|
|
12
|
+
targets = build_targets(order)
|
|
13
|
+
break if targets.empty?
|
|
14
|
+
|
|
15
|
+
# Pessimistic lock + fresh read of count_on_hand. The lock serializes
|
|
16
|
+
# concurrent checkouts and we use the locked rows below so we never
|
|
17
|
+
# check stock against a stale association cache.
|
|
18
|
+
locked_stock_items = Spree::StockItem
|
|
19
|
+
.where(id: targets.map { |_, si| si.id })
|
|
20
|
+
.lock
|
|
21
|
+
.index_by(&:id)
|
|
22
|
+
|
|
23
|
+
held = held_by_others(locked_stock_items.keys, order.id)
|
|
24
|
+
existing = existing_reservations_for(targets)
|
|
25
|
+
|
|
26
|
+
this_order_used = Hash.new(0)
|
|
27
|
+
|
|
28
|
+
targets.each do |line_item, stock_item|
|
|
29
|
+
stock_item = locked_stock_items.fetch(stock_item.id)
|
|
30
|
+
available = stock_item.count_on_hand - held.fetch(stock_item.id, 0) - this_order_used[stock_item.id]
|
|
31
|
+
|
|
32
|
+
if available < line_item.quantity
|
|
33
|
+
raise InsufficientStockError.new(
|
|
34
|
+
line_item,
|
|
35
|
+
Spree.t(
|
|
36
|
+
:insufficient_stock_for_reservation,
|
|
37
|
+
default: '%{item} has only %{available} available',
|
|
38
|
+
item: line_item.variant.name,
|
|
39
|
+
available: [available, 0].max
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
this_order_used[stock_item.id] += line_item.quantity
|
|
45
|
+
|
|
46
|
+
reservation = existing[[stock_item.id, line_item.id]] ||
|
|
47
|
+
Spree::StockReservation.new(stock_item: stock_item, line_item: line_item)
|
|
48
|
+
reservation.order = order
|
|
49
|
+
reservation.quantity = line_item.quantity
|
|
50
|
+
reservation.expires_at = expires_at
|
|
51
|
+
reservation.save!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
success(order)
|
|
56
|
+
rescue InsufficientStockError => e
|
|
57
|
+
failure(e.line_item, e.message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_targets(order)
|
|
63
|
+
order.line_items.includes(variant: { stock_items: :stock_location }).filter_map do |line_item|
|
|
64
|
+
variant = line_item.variant
|
|
65
|
+
next unless variant&.should_track_inventory?
|
|
66
|
+
|
|
67
|
+
stock_item = select_stock_item(variant)
|
|
68
|
+
next if stock_item.nil? || stock_item.backorderable?
|
|
69
|
+
|
|
70
|
+
[line_item, stock_item]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def select_stock_item(variant)
|
|
75
|
+
variant.stock_items.detect { |si| si.stock_location&.active? && si.available? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def held_by_others(stock_item_ids, exclude_order_id)
|
|
79
|
+
return {} if stock_item_ids.empty?
|
|
80
|
+
|
|
81
|
+
Spree::StockReservation
|
|
82
|
+
.active
|
|
83
|
+
.where(stock_item_id: stock_item_ids)
|
|
84
|
+
.where.not(order_id: exclude_order_id)
|
|
85
|
+
.group(:stock_item_id)
|
|
86
|
+
.sum(:quantity)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# One SELECT for all (stock_item_id, line_item_id) pairs we need to
|
|
90
|
+
# upsert. Returns a hash keyed by [stock_item_id, line_item_id].
|
|
91
|
+
def existing_reservations_for(targets)
|
|
92
|
+
return {} if targets.empty?
|
|
93
|
+
|
|
94
|
+
stock_item_ids = targets.map { |_, si| si.id }
|
|
95
|
+
line_item_ids = targets.map { |li, _| li.id }
|
|
96
|
+
|
|
97
|
+
Spree::StockReservation
|
|
98
|
+
.where(stock_item_id: stock_item_ids, line_item_id: line_item_ids)
|
|
99
|
+
.index_by { |r| [r.stock_item_id, r.line_item_id] }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -36,7 +36,7 @@ module Spree
|
|
|
36
36
|
|
|
37
37
|
if classifications_params.any?
|
|
38
38
|
opts = {}
|
|
39
|
-
opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless
|
|
39
|
+
opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless mysql_adapter?
|
|
40
40
|
|
|
41
41
|
Spree::Classification.upsert_all(
|
|
42
42
|
classifications_params,
|
|
@@ -58,6 +58,12 @@ module Spree
|
|
|
58
58
|
|
|
59
59
|
success(true)
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def mysql_adapter?
|
|
65
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
|
|
66
|
+
end
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
# Rails.application.config.filter_parameters.
|
|
10
10
|
#
|
|
11
11
|
# @example Output
|
|
12
|
-
# [Spree Event] order.
|
|
12
|
+
# [Spree Event] order.completed | payload: {"id"=>1} | 0.5ms
|
|
13
13
|
#
|
|
14
14
|
class EventLogSubscriber
|
|
15
15
|
NAMESPACE = 'spree'
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
|
-
# Handles order completion events to update product metrics
|
|
5
|
-
#
|
|
6
|
-
# When an order is completed, this subscriber enqueues background jobs
|
|
7
|
-
# to refresh the metrics (units_sold_count, revenue) for each product
|
|
8
|
-
# in the order.
|
|
4
|
+
# Handles order completion events to update product metrics
|
|
5
|
+
# (+units_sold_count+, +revenue+).
|
|
9
6
|
class ProductMetricsSubscriber < Spree::Subscriber
|
|
10
7
|
subscribes_to 'order.completed'
|
|
11
8
|
|
|
@@ -17,12 +14,11 @@ module Spree
|
|
|
17
14
|
|
|
18
15
|
order = Spree::Order.find_by_param(order_id)
|
|
19
16
|
return unless order
|
|
20
|
-
return unless order.store_id
|
|
21
17
|
|
|
22
18
|
product_ids = order.line_items.includes(:variant).map { |li| li.variant.product_id }.uniq
|
|
23
19
|
return if product_ids.empty?
|
|
24
20
|
|
|
25
|
-
jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id
|
|
21
|
+
jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id) }
|
|
26
22
|
ActiveJob.perform_all_later(jobs)
|
|
27
23
|
end
|
|
28
24
|
end
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
<p>
|
|
13
13
|
<%= link_to Spree.t(:accept), spree.admin_invitation_url(@invitation, token: @invitation.token, host: @invitation.store.formatted_url) %>
|
|
14
14
|
</p>
|
|
15
|
+
<% else %>
|
|
16
|
+
<p>
|
|
17
|
+
<%= link_to Spree.t(:accept), main_app.admin_invitation_acceptance_url(@invitation) %>
|
|
18
|
+
</p>
|
|
15
19
|
<% end %>
|
|
16
20
|
|
|
17
21
|
<p>
|
data/config/locales/en.yml
CHANGED
|
@@ -299,6 +299,11 @@ en:
|
|
|
299
299
|
attributes:
|
|
300
300
|
currency:
|
|
301
301
|
must_match_order_currency: Must match order currency
|
|
302
|
+
spree/market:
|
|
303
|
+
attributes:
|
|
304
|
+
base:
|
|
305
|
+
cannot_destroy_default_market: Cannot delete the default market.
|
|
306
|
+
cannot_destroy_last_market: Cannot delete the only market.
|
|
302
307
|
spree/market_country:
|
|
303
308
|
attributes:
|
|
304
309
|
country:
|
|
@@ -318,6 +323,10 @@ en:
|
|
|
318
323
|
cannot_destroy_if_attached_to_line_items: Cannot delete Products that are added to placed Orders. In such cases, please discontinue them.
|
|
319
324
|
discontinue_on:
|
|
320
325
|
invalid_date_range: must be later than available date
|
|
326
|
+
spree/product_publication:
|
|
327
|
+
attributes:
|
|
328
|
+
unpublished_at:
|
|
329
|
+
must_be_after_published_at: must be after published date
|
|
321
330
|
spree/promotion:
|
|
322
331
|
attributes:
|
|
323
332
|
expires_at:
|
|
@@ -780,6 +789,8 @@ en:
|
|
|
780
789
|
are_you_sure: Are you sure?
|
|
781
790
|
are_you_sure_delete: Are you sure you want to delete this record?
|
|
782
791
|
assets: Media
|
|
792
|
+
assigned_variants: Assigned variants
|
|
793
|
+
assigned_variants_help: Pick the variants this image represents. Leave blank to apply to all variants.
|
|
783
794
|
associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
|
|
784
795
|
at_symbol: "@"
|
|
785
796
|
attachments: Attachments
|
|
@@ -874,7 +885,8 @@ en:
|
|
|
874
885
|
change: Change
|
|
875
886
|
change_password: Change password
|
|
876
887
|
changes_published: Changes published!
|
|
877
|
-
channel:
|
|
888
|
+
channel: Sales channel
|
|
889
|
+
channels: Sales channels
|
|
878
890
|
charged: Charged
|
|
879
891
|
checkout: Checkout
|
|
880
892
|
checkout_message: Checkout message
|
|
@@ -1124,8 +1136,12 @@ en:
|
|
|
1124
1136
|
errors:
|
|
1125
1137
|
messages:
|
|
1126
1138
|
blank: can't be blank
|
|
1139
|
+
cannot_delete_default_channel: Default channel cannot be deleted. Promote another channel to default first.
|
|
1127
1140
|
cannot_remove_icon: Cannot remove image
|
|
1141
|
+
channel_store_mismatch: must belong to the same store
|
|
1128
1142
|
could_not_create_taxon: Could not create taxon
|
|
1143
|
+
invalid_order_routing_rule: is not a registered order routing rule
|
|
1144
|
+
invalid_order_routing_strategy: is not a registered order routing strategy
|
|
1129
1145
|
must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
|
|
1130
1146
|
no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
|
|
1131
1147
|
store_association_can_not_be_changed: The store association can not be changed
|
|
@@ -1311,6 +1327,9 @@ en:
|
|
|
1311
1327
|
inventory_error_flash_for_insufficient_quantity: An item in your cart has become unavailable.
|
|
1312
1328
|
inventory_state: Inventory State
|
|
1313
1329
|
invitation_accepted: Invitation accepted!
|
|
1330
|
+
invitation_expired:
|
|
1331
|
+
body: This invitation link is no longer valid. Please ask the person who invited you to send a new one.
|
|
1332
|
+
heading: Invitation invalid or expired
|
|
1314
1333
|
invitation_mailer:
|
|
1315
1334
|
greeting: Hi %{user_name},
|
|
1316
1335
|
invitation_accepted:
|
|
@@ -1445,6 +1464,7 @@ en:
|
|
|
1445
1464
|
new_api_key: New API Key
|
|
1446
1465
|
new_balance: New balance
|
|
1447
1466
|
new_billing_address: New Billing Address
|
|
1467
|
+
new_channel: New sales channel
|
|
1448
1468
|
new_country: New Country
|
|
1449
1469
|
new_custom_domain: New Custom Domain
|
|
1450
1470
|
new_customer: New Customer
|
|
@@ -1633,6 +1653,10 @@ en:
|
|
|
1633
1653
|
order_number: Order %{number}
|
|
1634
1654
|
order_processed_successfully: Your order has been processed successfully
|
|
1635
1655
|
order_resumed: Order resumed
|
|
1656
|
+
order_routing:
|
|
1657
|
+
strategies:
|
|
1658
|
+
legacy: Legacy
|
|
1659
|
+
rules: Rules (ordered)
|
|
1636
1660
|
order_state:
|
|
1637
1661
|
address: address
|
|
1638
1662
|
awaiting_return: awaiting return
|
|
@@ -1878,15 +1902,24 @@ en:
|
|
|
1878
1902
|
promotion_not_cloned: 'Promotion has not been cloned. Reason: %{error}'
|
|
1879
1903
|
promotion_rule: Promotion Rule
|
|
1880
1904
|
promotion_rule_types:
|
|
1905
|
+
category:
|
|
1906
|
+
description: Order includes products in specified categories
|
|
1907
|
+
name: Categories
|
|
1881
1908
|
country:
|
|
1882
1909
|
description: Limit to orders with shipping address in a specific country
|
|
1883
1910
|
name: Country
|
|
1884
1911
|
currency:
|
|
1885
1912
|
description: Limit to orders in a specific currency
|
|
1886
1913
|
name: Currency
|
|
1914
|
+
customer:
|
|
1915
|
+
description: Available only to the specified customers
|
|
1916
|
+
name: Customers
|
|
1887
1917
|
customer_group:
|
|
1888
1918
|
description: Available only to customers in specified customer group(s)
|
|
1889
1919
|
name: Customer Group(s)
|
|
1920
|
+
customer_logged_in:
|
|
1921
|
+
description: Available only to logged in customers
|
|
1922
|
+
name: Only logged in customers
|
|
1890
1923
|
first_order:
|
|
1891
1924
|
description: Must be the customer's first order
|
|
1892
1925
|
name: First order
|
|
@@ -1902,15 +1935,6 @@ en:
|
|
|
1902
1935
|
product:
|
|
1903
1936
|
description: Order includes specified product(s)
|
|
1904
1937
|
name: Product(s)
|
|
1905
|
-
taxon:
|
|
1906
|
-
description: Order includes products with specified taxon(s)
|
|
1907
|
-
name: Taxon(s)
|
|
1908
|
-
user:
|
|
1909
|
-
description: Available only to the specified customers
|
|
1910
|
-
name: User
|
|
1911
|
-
user_logged_in:
|
|
1912
|
-
description: Available only to logged in users
|
|
1913
|
-
name: User Logged In
|
|
1914
1938
|
promotion_uses: Promotion uses
|
|
1915
1939
|
promotionable: Promotable
|
|
1916
1940
|
promotions: Promotions
|
|
@@ -2479,6 +2503,7 @@ en:
|
|
|
2479
2503
|
view: View
|
|
2480
2504
|
view_all: View all
|
|
2481
2505
|
view_full_details: View full details
|
|
2506
|
+
view_full_size: View full size
|
|
2482
2507
|
view_store: View store
|
|
2483
2508
|
view_your_store: View your store
|
|
2484
2509
|
visibility: Visibility
|
data/config/routes.rb
CHANGED
|
@@ -40,6 +40,15 @@ Rails.application.routes.draw do
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
|
+
# Used by admin mailers; the SPA derives the URL from `window.location.origin` instead.
|
|
44
|
+
direct :admin_invitation_acceptance do |invitation, _options = {}|
|
|
45
|
+
path = "/accept-invitation/#{invitation.prefixed_id}?token=#{invitation.token}"
|
|
46
|
+
base = Spree::Config[:admin_url].presence ||
|
|
47
|
+
(Rails.env.development? ? 'http://localhost:5173' : nil) ||
|
|
48
|
+
invitation.store&.formatted_url
|
|
49
|
+
|
|
50
|
+
base.present? ? "#{base.chomp('/')}#{path}" : path
|
|
51
|
+
end
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
Spree::Core::Engine.draw_routes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class CreateSpreeOrderCancellations < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_order_cancellations do |t|
|
|
4
|
+
t.references :order, null: false, index: false
|
|
5
|
+
t.string :reason, null: false
|
|
6
|
+
t.text :note
|
|
7
|
+
t.boolean :restock_items, null: false
|
|
8
|
+
t.boolean :refund_payments, null: false
|
|
9
|
+
t.decimal :refund_amount, precision: 10, scale: 2
|
|
10
|
+
t.boolean :notify_customer, null: false
|
|
11
|
+
t.references :canceled_by, polymorphic: true, index: false
|
|
12
|
+
if t.respond_to? :jsonb
|
|
13
|
+
t.jsonb :metadata
|
|
14
|
+
else
|
|
15
|
+
t.json :metadata
|
|
16
|
+
end
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :spree_order_cancellations, :order_id
|
|
21
|
+
add_index :spree_order_cancellations, [:canceled_by_id, :canceled_by_type],
|
|
22
|
+
name: 'idx_order_cancellations_canceled_by'
|
|
23
|
+
add_index :spree_order_cancellations, :created_at
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateSpreeOrderApprovals < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_order_approvals do |t|
|
|
4
|
+
t.references :order, null: false, index: false
|
|
5
|
+
t.string :status, null: false
|
|
6
|
+
t.string :level
|
|
7
|
+
t.text :note
|
|
8
|
+
t.references :approver, polymorphic: true, index: false
|
|
9
|
+
t.datetime :decided_at
|
|
10
|
+
if t.respond_to? :jsonb
|
|
11
|
+
t.jsonb :metadata
|
|
12
|
+
else
|
|
13
|
+
t.json :metadata
|
|
14
|
+
end
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :spree_order_approvals, [:order_id, :status]
|
|
19
|
+
add_index :spree_order_approvals, [:approver_id, :approver_type],
|
|
20
|
+
name: 'idx_order_approvals_approver'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateSpreeStockReservations < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_stock_reservations do |t|
|
|
4
|
+
t.references :stock_item, null: false, index: false
|
|
5
|
+
t.references :line_item, null: false
|
|
6
|
+
t.references :order, null: false
|
|
7
|
+
t.integer :quantity, null: false
|
|
8
|
+
t.datetime :expires_at, null: false
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Composite (stock_item_id, expires_at) is the hot-path Quantifier query;
|
|
13
|
+
# leading on stock_item_id makes it cover the standalone-by-stock-item lookup too.
|
|
14
|
+
add_index :spree_stock_reservations, [:stock_item_id, :expires_at]
|
|
15
|
+
add_index :spree_stock_reservations, [:stock_item_id, :line_item_id], unique: true,
|
|
16
|
+
name: 'idx_stock_reservations_item_line_item'
|
|
17
|
+
add_index :spree_stock_reservations, :expires_at
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateSpreeVariantMedia < ActiveRecord::Migration[7.2]
|
|
2
|
+
# `media_id` references spree_assets.id in 5.5 — the parent table renames to
|
|
3
|
+
# spree_media in 6.0 (see 5.4-6.0-product-media-system.md Phase 3 of the 6.0
|
|
4
|
+
# cleanup). Using `media_id` from the start saves a column rename in 6.0.
|
|
5
|
+
#
|
|
6
|
+
# Table name is `spree_variant_media` (not the Rails default `spree_variant_medias`)
|
|
7
|
+
# since "media" reads as collective; the model overrides `self.table_name`.
|
|
8
|
+
#
|
|
9
|
+
# No per-variant `position` column — gallery order comes from the asset's
|
|
10
|
+
# product-level position (Asset#position). Variant gallery resolution filters
|
|
11
|
+
# product media down to the linked subset and inherits the product's order.
|
|
12
|
+
def change
|
|
13
|
+
create_table :spree_variant_media do |t|
|
|
14
|
+
t.references :variant, null: false, index: false
|
|
15
|
+
t.bigint :media_id, null: false
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :spree_variant_media, [:variant_id, :media_id], unique: true,
|
|
20
|
+
name: 'idx_variant_media_unique'
|
|
21
|
+
add_index :spree_variant_media, :media_id, name: 'idx_variant_media_media'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AddPickupToSpreeStockLocations < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
add_column :spree_stock_locations, :kind, :string, null: false, default: 'warehouse'
|
|
4
|
+
add_column :spree_stock_locations, :pickup_enabled, :boolean, null: false, default: false
|
|
5
|
+
add_column :spree_stock_locations, :pickup_stock_policy, :string, null: false, default: 'local'
|
|
6
|
+
add_column :spree_stock_locations, :pickup_ready_in_minutes, :integer
|
|
7
|
+
add_column :spree_stock_locations, :pickup_instructions, :text
|
|
8
|
+
|
|
9
|
+
add_index :spree_stock_locations, :pickup_enabled
|
|
10
|
+
add_index :spree_stock_locations, :kind
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateSpreeChannels < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_channels do |t|
|
|
4
|
+
t.references :store, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :code, null: false
|
|
7
|
+
t.boolean :active, null: false
|
|
8
|
+
t.text :preferences
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :spree_channels, %i[store_id code], unique: true
|
|
13
|
+
|
|
14
|
+
# Default-channel backfill for existing stores lives in
|
|
15
|
+
# +rake spree:channels:create_defaults+ (data transformations don't belong
|
|
16
|
+
# in migrations per Spree's guidelines).
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateSpreeOrderRoutingRules < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_order_routing_rules do |t|
|
|
4
|
+
t.references :store, null: false
|
|
5
|
+
t.references :channel, null: false
|
|
6
|
+
t.string :type, null: false
|
|
7
|
+
t.integer :position, null: false
|
|
8
|
+
t.boolean :active, null: false
|
|
9
|
+
t.text :preferences
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :spree_order_routing_rules, [:channel_id, :position]
|
|
14
|
+
add_index :spree_order_routing_rules, [:channel_id, :active, :position],
|
|
15
|
+
name: 'idx_order_routing_rules_lookup'
|
|
16
|
+
add_index :spree_order_routing_rules, :type
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddChannelIdToSpreeOrders < ActiveRecord::Migration[7.2]
|
|
2
|
+
# Non-destructive: adds the FK column alongside the existing `channel`
|
|
3
|
+
# string column. Old reads keep working until the upgrade rake task
|
|
4
|
+
# `spree:order_routing:backfill_channel_ids` populates `channel_id`.
|
|
5
|
+
# The string column is dropped in a follow-up migration once the
|
|
6
|
+
# backfill has run.
|
|
7
|
+
def change
|
|
8
|
+
add_reference :spree_orders, :channel
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class BackfillStatusOnSpreeOrders < ActiveRecord::Migration[7.2]
|
|
2
|
+
BATCH_SIZE = 10_000
|
|
3
|
+
|
|
4
|
+
STATE_TO_STATUS = {
|
|
5
|
+
'placed' => %w[complete resumed returned awaiting_return],
|
|
6
|
+
'canceled' => %w[canceled partially_canceled]
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
def up
|
|
10
|
+
STATE_TO_STATUS.each do |target_status, states|
|
|
11
|
+
backfill_in_batches(target_status, states)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Anything still NULL (carts, in-progress checkouts) becomes draft.
|
|
15
|
+
backfill_in_batches('draft', nil)
|
|
16
|
+
|
|
17
|
+
change_column_default :spree_orders, :status, 'draft'
|
|
18
|
+
change_column_null :spree_orders, :status, false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def down
|
|
22
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
23
|
+
'Backfilled status values cannot be safely restored: writes made after deploy ' \
|
|
24
|
+
'are indistinguishable from the backfill. Roll back manually if needed.'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def backfill_in_batches(target_status, source_states)
|
|
30
|
+
quoted_status = connection.quote(target_status)
|
|
31
|
+
state_filter =
|
|
32
|
+
if source_states
|
|
33
|
+
quoted = Array(source_states).map { |s| connection.quote(s) }.join(',')
|
|
34
|
+
"AND state IN (#{quoted})"
|
|
35
|
+
else
|
|
36
|
+
''
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
loop do
|
|
40
|
+
# The nested SELECT wrap (`SELECT id FROM (SELECT id ... LIMIT N) AS t`) is
|
|
41
|
+
# required for MySQL, which refuses an UPDATE that selects from the same
|
|
42
|
+
# table in the WHERE clause without an intermediate derived table.
|
|
43
|
+
affected = connection.update(<<~SQL.squish)
|
|
44
|
+
UPDATE spree_orders SET status = #{quoted_status}
|
|
45
|
+
WHERE id IN (
|
|
46
|
+
SELECT id FROM (
|
|
47
|
+
SELECT id FROM spree_orders
|
|
48
|
+
WHERE status IS NULL #{state_filter}
|
|
49
|
+
LIMIT #{BATCH_SIZE}
|
|
50
|
+
) AS spree_orders_batch
|
|
51
|
+
)
|
|
52
|
+
SQL
|
|
53
|
+
|
|
54
|
+
break if affected < BATCH_SIZE
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|