spree_core 5.4.3 → 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 +2 -1
- 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/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 +28 -5
- 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/export.rb +30 -3
- 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 +6 -1
- data/app/models/spree/market.rb +32 -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_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/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/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/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/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/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/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/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +27 -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/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/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 +83 -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
|
@@ -56,6 +56,7 @@ module Spree
|
|
|
56
56
|
:shipment_attributes,
|
|
57
57
|
:shipping_method_attributes,
|
|
58
58
|
:shipping_category_attributes,
|
|
59
|
+
:channel_attributes,
|
|
59
60
|
:source_attributes,
|
|
60
61
|
:stock_item_attributes,
|
|
61
62
|
:stock_location_attributes,
|
|
@@ -90,10 +91,9 @@ module Spree
|
|
|
90
91
|
|
|
91
92
|
@@allowed_origin_attributes = [:origin]
|
|
92
93
|
|
|
93
|
-
@@api_key_attributes = [:name, :key_type]
|
|
94
|
+
@@api_key_attributes = [:name, :key_type, { scopes: [] }]
|
|
94
95
|
|
|
95
|
-
@@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position,
|
|
96
|
-
:media_type, :focal_point_x, :focal_point_y, :external_video_url]
|
|
96
|
+
@@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position, :url, :signed_id]
|
|
97
97
|
|
|
98
98
|
@@checkout_attributes = [
|
|
99
99
|
:coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing, :use_shipping,
|
|
@@ -169,7 +169,7 @@ module Spree
|
|
|
169
169
|
|
|
170
170
|
@@payment_attributes = [:amount, :payment_method_id, :payment_method]
|
|
171
171
|
|
|
172
|
-
@@payment_method_attributes = [:name, :type, :description, :active, :display_on, :auto_capture, :position]
|
|
172
|
+
@@payment_method_attributes = [:name, :type, :description, :active, :display_on, :auto_capture, :position, { metadata: {}, preferences: {} }]
|
|
173
173
|
|
|
174
174
|
@@payment_session_attributes = [:amount, :payment_method_id, { external_data: {} }]
|
|
175
175
|
|
|
@@ -192,8 +192,8 @@ module Spree
|
|
|
192
192
|
label_list: [],
|
|
193
193
|
option_type_ids: [],
|
|
194
194
|
taxon_ids: [],
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
product_option_types_attributes: [:id, :option_type_id, :position, :_destroy],
|
|
196
|
+
legacy_product_publications_attributes: [:id, :channel_id, :published_at, :unpublished_at, :_destroy]
|
|
197
197
|
}
|
|
198
198
|
]
|
|
199
199
|
|
|
@@ -240,6 +240,8 @@ module Spree
|
|
|
240
240
|
|
|
241
241
|
@@shipping_category_attributes = [:name]
|
|
242
242
|
|
|
243
|
+
@@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy]
|
|
244
|
+
|
|
243
245
|
@@shipping_method_attributes = [:name, :admin_name, :code, :tracking_url, :tax_category_id, :display_on,
|
|
244
246
|
:estimated_transit_business_days_min, :estimated_transit_business_days_max,
|
|
245
247
|
:calculator_type, :preferences, zone_ids: [], shipping_category_ids: [], calculator_attributes: {}]
|
|
@@ -251,7 +253,7 @@ module Spree
|
|
|
251
253
|
:gateway_payment_profile_id, :last_digits, :name, :encrypted_data
|
|
252
254
|
]
|
|
253
255
|
|
|
254
|
-
@@stock_item_attributes = [:variant_id, :stock_location_id, :backorderable, :count_on_hand]
|
|
256
|
+
@@stock_item_attributes = [:variant_id, :stock_location_id, :backorderable, :count_on_hand, { metadata: {} }]
|
|
255
257
|
|
|
256
258
|
@@stock_location_attributes = [
|
|
257
259
|
:name, :active, :address1, :address2, :city, :zipcode, :company,
|
|
@@ -5,19 +5,26 @@ FactoryBot.define do
|
|
|
5
5
|
company { 'Company' }
|
|
6
6
|
sequence(:address1) { |n| "#{n} Lovely Street" }
|
|
7
7
|
address2 { 'Northwest' }
|
|
8
|
-
city { '
|
|
9
|
-
zipcode { '
|
|
8
|
+
city { 'New York' }
|
|
9
|
+
zipcode { '10118' }
|
|
10
10
|
phone { '555-555-0199' }
|
|
11
11
|
alternative_phone { '555-555-0199' }
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
country
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
# Default to a real US/NY pair (cached via find_or_create_by) so generated
|
|
14
|
+
# OpenAPI examples carry plausible country/state fields. Tests that need a
|
|
15
|
+
# different state/country pass them explicitly.
|
|
16
|
+
country do
|
|
17
|
+
Spree::Country.find_or_create_by!(iso: 'US') do |c|
|
|
18
|
+
c.iso3 = 'USA'
|
|
19
|
+
c.name = 'United States of America'
|
|
20
|
+
c.iso_name = 'UNITED STATES'
|
|
21
|
+
c.numcode = 840
|
|
22
|
+
c.states_required = true
|
|
20
23
|
end
|
|
21
24
|
end
|
|
25
|
+
|
|
26
|
+
state do |address|
|
|
27
|
+
(address.country || Spree::Country.find_by(iso: 'US'))&.states&.find_or_create_by!(abbr: 'NY') { |s| s.name = 'New York' }
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
end
|
|
@@ -8,14 +8,8 @@ FactoryBot.define do
|
|
|
8
8
|
product { nil }
|
|
9
9
|
end
|
|
10
10
|
variant do
|
|
11
|
-
resolved_product = product ||
|
|
12
|
-
|
|
13
|
-
create(:product, stores: [order.store])
|
|
14
|
-
else
|
|
15
|
-
create(:product)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
resolved_product.master
|
|
11
|
+
resolved_product = product || create(:product)
|
|
12
|
+
resolved_product.default_variant
|
|
19
13
|
end
|
|
20
14
|
end
|
|
21
15
|
end
|
|
@@ -4,12 +4,10 @@ FactoryBot.define do
|
|
|
4
4
|
description { generate(:random_description) }
|
|
5
5
|
cost_price { 17.00 }
|
|
6
6
|
sku { generate(:sku) }
|
|
7
|
-
available_on { 1.year.ago }
|
|
8
|
-
make_active_at { 1.year.ago }
|
|
9
7
|
deleted_at { nil }
|
|
10
8
|
shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) }
|
|
11
9
|
status { 'active' }
|
|
12
|
-
|
|
10
|
+
store { Spree::Store.default || association(:store) }
|
|
13
11
|
|
|
14
12
|
transient do
|
|
15
13
|
price { 19.99 }
|
|
@@ -17,11 +15,8 @@ FactoryBot.define do
|
|
|
17
15
|
currency { nil }
|
|
18
16
|
end
|
|
19
17
|
|
|
20
|
-
# ensure stock item will be created for this products master
|
|
21
|
-
# also attach this product to the default store if no stores are passed in
|
|
22
18
|
before(:create) do |_product|
|
|
23
19
|
create(:stock_location) unless Spree::StockLocation.any?
|
|
24
|
-
create(:store, default: true) unless Spree::Store.any?
|
|
25
20
|
end
|
|
26
21
|
after(:create) do |product, evaluator|
|
|
27
22
|
existing_location_ids = product.master.stock_items.pluck(:stock_location_id)
|
|
@@ -30,9 +25,23 @@ FactoryBot.define do
|
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
if evaluator.price.present?
|
|
33
|
-
price_currency = evaluator.currency || product.
|
|
28
|
+
price_currency = evaluator.currency || product.store&.default_currency || 'USD'
|
|
34
29
|
product.master.set_price(price_currency, evaluator.price, evaluator.compare_at_price)
|
|
35
30
|
end
|
|
31
|
+
|
|
32
|
+
# Test convenience only: auto-publish each product on its store's
|
|
33
|
+
# default channel so legacy spec assertions that depend on
|
|
34
|
+
# current-channel visibility (.active, .available, .not_discontinued)
|
|
35
|
+
# keep passing. Production callers must publish explicitly via the
|
|
36
|
+
# Admin SDK / Dashboard create form.
|
|
37
|
+
if product.store&.default_channel && product.product_publications.empty?
|
|
38
|
+
Spree::ProductPublication.create!(
|
|
39
|
+
product: product,
|
|
40
|
+
channel: product.store.default_channel,
|
|
41
|
+
published_at: product.available_on,
|
|
42
|
+
unpublished_at: product.discontinue_on
|
|
43
|
+
)
|
|
44
|
+
end
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
factory :custom_product do
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :refresh_token, class: 'Spree::RefreshToken' do
|
|
3
|
+
association :user, factory: :user
|
|
4
|
+
user_type { user.class.to_s }
|
|
5
|
+
expires_at { Spree::RefreshToken.default_expiry.from_now }
|
|
6
|
+
|
|
7
|
+
trait :for_admin do
|
|
8
|
+
association :user, factory: :admin_user
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
trait :expired do
|
|
12
|
+
expires_at { 1.minute.ago }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -19,8 +19,8 @@ FactoryBot.define do
|
|
|
19
19
|
# variant will add itself to all stock_locations in an after_create
|
|
20
20
|
# creating a product will automatically create a master variant
|
|
21
21
|
store = Spree::Store.first || create(:store)
|
|
22
|
-
product_1 = create(:product
|
|
23
|
-
product_2 = create(:product
|
|
22
|
+
product_1 = create(:product)
|
|
23
|
+
product_2 = create(:product)
|
|
24
24
|
|
|
25
25
|
stock_location.stock_item_or_create(product_1.master).adjust_count_on_hand(10)
|
|
26
26
|
stock_location.stock_item_or_create(product_2.master).adjust_count_on_hand(20)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :stock_reservation, class: Spree::StockReservation do
|
|
3
|
+
quantity { 1 }
|
|
4
|
+
expires_at { 10.minutes.from_now }
|
|
5
|
+
|
|
6
|
+
transient do
|
|
7
|
+
order { nil }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Build the order first (with at least one line_item), then derive
|
|
11
|
+
# stock_item from that line_item's variant so the three FKs reference the
|
|
12
|
+
# same variant. Callers can override stock_item:/line_item:/order: to wire
|
|
13
|
+
# up a specific scenario.
|
|
14
|
+
after(:build) do |reservation, evaluator|
|
|
15
|
+
reservation.order ||= evaluator.order || create(:order_with_line_items, line_items_count: 1)
|
|
16
|
+
|
|
17
|
+
if reservation.line_item.nil?
|
|
18
|
+
reservation.line_item = reservation.order.line_items.first ||
|
|
19
|
+
create(:line_item, order: reservation.order)
|
|
20
|
+
reservation.order.line_items.reload
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
reservation.stock_item ||= reservation.line_item.variant.stock_items.first ||
|
|
24
|
+
create(:stock_item, variant: reservation.line_item.variant)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
trait :expired do
|
|
28
|
+
expires_at { 1.minute.ago }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -11,7 +11,7 @@ FactoryBot.define do
|
|
|
11
11
|
is_master { 0 }
|
|
12
12
|
track_inventory { true }
|
|
13
13
|
|
|
14
|
-
product { |p| p.association(:base_product
|
|
14
|
+
product { |p| p.association(:base_product) }
|
|
15
15
|
option_values { [build(:option_value)] }
|
|
16
16
|
|
|
17
17
|
transient do
|
|
@@ -35,14 +35,14 @@ FactoryBot.define do
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
if evaluator.price.present?
|
|
38
|
-
price_currency = evaluator.currency || variant.product&.
|
|
38
|
+
price_currency = evaluator.currency || variant.product&.store&.default_currency || 'USD'
|
|
39
39
|
variant.set_price(price_currency, evaluator.price, evaluator.compare_at_price)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
factory :variant do
|
|
44
44
|
# on_hand 5
|
|
45
|
-
product { |p| p.association(:product
|
|
45
|
+
product { |p| p.association(:product) }
|
|
46
46
|
|
|
47
47
|
factory :with_image_variant do
|
|
48
48
|
images { create_list(:image, 1) }
|
|
@@ -4,7 +4,7 @@ class OrderWalkthrough
|
|
|
4
4
|
|
|
5
5
|
# A payment method must exist for an order to proceed through the Address state
|
|
6
6
|
unless Spree::PaymentMethod.exists?
|
|
7
|
-
FactoryBot.create(:check_payment_method
|
|
7
|
+
FactoryBot.create(:check_payment_method)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
# Need to create a valid zone too...
|
|
@@ -31,6 +31,16 @@ RSpec.configure do |config|
|
|
|
31
31
|
@default_store&.promotions = []
|
|
32
32
|
@default_store&.update_column(:checkout_zone_id, nil) if @default_store&.read_attribute(:checkout_zone_id).present?
|
|
33
33
|
@default_store&.payment_methods = []
|
|
34
|
+
# The shared +@default_store+ Ruby object lives across the whole
|
|
35
|
+
# +before(:all)+ block, so AR association caches (+default_market+,
|
|
36
|
+
# +channels+, etc.) and per-instance memos (+@has_markets+) need to
|
|
37
|
+
# be cleared between examples or stale +nil+s leak across tests.
|
|
38
|
+
if @default_store
|
|
39
|
+
@default_store.association(:default_market).reset if @default_store.association_cached?(:default_market)
|
|
40
|
+
@default_store.association(:markets).reset if @default_store.association_cached?(:markets)
|
|
41
|
+
@default_store.remove_instance_variable(:@has_markets) if @default_store.instance_variable_defined?(:@has_markets)
|
|
42
|
+
@default_store.reload
|
|
43
|
+
end
|
|
34
44
|
end
|
|
35
45
|
end
|
|
36
46
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Spree 5.4 → 5.5 upgrade manifest.
|
|
2
|
+
#
|
|
3
|
+
# Lists ONLY the version-specific rake tasks that perform data backfills.
|
|
4
|
+
# Universal upgrade steps (bundle update, db:migrate, scheduling cron jobs,
|
|
5
|
+
# reviewing breaking changes) are NOT in this file:
|
|
6
|
+
#
|
|
7
|
+
# - `bundle update` + `db:migrate` are handled by your deploy pipeline
|
|
8
|
+
# (Heroku release phase, K8s init container, Render auto-migrate,
|
|
9
|
+
# Capistrano deploy hook, etc.) and by the @spree/cli's `spree upgrade`
|
|
10
|
+
# wrapper for local development.
|
|
11
|
+
# - Cron scheduling, optional tunings, and human-readable behavior changes
|
|
12
|
+
# live in the upgrade doc at docs/developer/upgrades/5.4-to-5.5.mdx.
|
|
13
|
+
#
|
|
14
|
+
# This manifest is the machine-runnable shape — what `bin/rake spree:upgrade`
|
|
15
|
+
# (in production) and `spree upgrade` (in dev) execute. Every step must be a
|
|
16
|
+
# rake task and must be idempotent. Re-running the full manifest is safe.
|
|
17
|
+
---
|
|
18
|
+
from: "5.4"
|
|
19
|
+
to: "5.5"
|
|
20
|
+
docs: "https://spreecommerce.org/docs/developer/upgrades/5.4-to-5.5"
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- id: media
|
|
24
|
+
name: "Migrate legacy variant-pinned images to product-level media"
|
|
25
|
+
task: "spree:media:migrate_master_images_to_product_media"
|
|
26
|
+
notes: |
|
|
27
|
+
Enqueues one `Spree::Media::MigrateProductAssetsJob` per product onto
|
|
28
|
+
the `images` queue — confirm your job runner is processing that queue.
|
|
29
|
+
Storefront keeps working while jobs drain (old assets stay variant-pinned
|
|
30
|
+
until each job finishes). For large catalogs, tune with BATCH_SIZE=1000.
|
|
31
|
+
|
|
32
|
+
- id: channels
|
|
33
|
+
name: "Run the Channels upgrade (creates default channels, publications, order channel ids)"
|
|
34
|
+
task: "spree:channels:upgrade"
|
|
35
|
+
notes: |
|
|
36
|
+
Aggregator that runs four sub-tasks in order:
|
|
37
|
+
1. spree:channels:create_defaults
|
|
38
|
+
2. spree:upgrade:populate_publications
|
|
39
|
+
3. spree:channels:backfill_order_channel_ids
|
|
40
|
+
4. spree:channels:backfill_product_publication_dates
|
|
41
|
+
Until this runs, every product has store_id IS NULL and is invisible
|
|
42
|
+
to Product.for_store — admin lists, storefront catalog, and search
|
|
43
|
+
indexer all return empty.
|
|
44
|
+
|
|
45
|
+
- id: reindex
|
|
46
|
+
name: "Reindex products against the configured search provider"
|
|
47
|
+
task: "spree:search:reindex"
|
|
48
|
+
notes: |
|
|
49
|
+
No-op for the default Database provider. For Meilisearch (or any other
|
|
50
|
+
external search provider), this is required after the channels upgrade
|
|
51
|
+
because products only become visible to Product.for_store once they
|
|
52
|
+
have a store_id — reindexing before the channels step would index 0
|
|
53
|
+
products. Must run AFTER `channels`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
namespace :spree do
|
|
2
|
+
namespace :channels do
|
|
3
|
+
desc 'Create the default channel for every existing store (idempotent — calls Store#ensure_default_channel).'
|
|
4
|
+
task create_defaults: :environment do
|
|
5
|
+
created = 0
|
|
6
|
+
Spree::Store.find_each do |store|
|
|
7
|
+
next if store.default_channel
|
|
8
|
+
|
|
9
|
+
store.ensure_default_channel
|
|
10
|
+
created += 1
|
|
11
|
+
puts " Created default channel for store '#{store.name}'"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
puts created.zero? ? ' All stores already have a default channel.' : " Created #{created} default channel(s)."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'Backfill spree_orders.channel_id from the legacy spree_orders.channel string column'
|
|
18
|
+
task backfill_order_channel_ids: :environment do
|
|
19
|
+
# Idempotent: only touches orders where channel_id is nil. Safe to
|
|
20
|
+
# re-run after partial completion. Returns gracefully if the legacy
|
|
21
|
+
# string column has already been dropped.
|
|
22
|
+
unless legacy_channel_column?
|
|
23
|
+
puts 'Legacy channel column not present — backfill is unnecessary.'
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Spree::Store.find_each do |store|
|
|
28
|
+
legacy_codes = Spree::Order.where(store_id: store.id, channel_id: nil)
|
|
29
|
+
.distinct
|
|
30
|
+
.pluck(Arel.sql('channel'))
|
|
31
|
+
.compact_blank
|
|
32
|
+
|
|
33
|
+
codes_to_process = legacy_codes.uniq
|
|
34
|
+
codes_to_process << Spree::Channel::DEFAULT_CODE unless codes_to_process.include?(Spree::Channel::DEFAULT_CODE)
|
|
35
|
+
|
|
36
|
+
codes_to_process.each do |code|
|
|
37
|
+
channel = store.channels.find_or_create_by!(code: code) do |c|
|
|
38
|
+
c.name = code.titleize
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
scope = Spree::Order.where(store_id: store.id, channel_id: nil)
|
|
42
|
+
scope = if code == Spree::Channel::DEFAULT_CODE
|
|
43
|
+
# Only the default channel claims NULL/blank rows.
|
|
44
|
+
scope.where(Arel.sql("channel = ? OR channel IS NULL OR channel = ''"), code)
|
|
45
|
+
else
|
|
46
|
+
scope.where(Arel.sql('channel = ?'), code)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
updated = scope.update_all(channel_id: channel.id)
|
|
50
|
+
|
|
51
|
+
next if updated.zero?
|
|
52
|
+
|
|
53
|
+
puts " Store '#{store.name}': mapped #{updated} orders with channel='#{code}' → #{channel.name} (#{channel.code})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc 'Backfill published_at and unpublished_at on ProductPublications from the legacy Product.available_on / discontinue_on columns'
|
|
59
|
+
task backfill_product_publication_dates: :environment do
|
|
60
|
+
# Per-product loop (not join-update) for SQLite/MySQL/Postgres portability.
|
|
61
|
+
published = 0
|
|
62
|
+
unpublished = 0
|
|
63
|
+
|
|
64
|
+
products_with_dates = Spree::Product.where.not(available_on: nil).or(Spree::Product.where.not(discontinue_on: nil))
|
|
65
|
+
|
|
66
|
+
products_with_dates.find_each(batch_size: 500) do |product|
|
|
67
|
+
publications = Spree::ProductPublication.where(product_id: product.id)
|
|
68
|
+
# Read raw columns — +product.available_on+ / +product.discontinue_on+
|
|
69
|
+
# go through +Product::Channels+'s reader override which prefers the
|
|
70
|
+
# current-channel publication's date (which is nil pre-backfill).
|
|
71
|
+
legacy_available_on = product[:available_on]
|
|
72
|
+
legacy_discontinue_on = product[:discontinue_on]
|
|
73
|
+
|
|
74
|
+
published += publications.where(published_at: nil).update_all(published_at: legacy_available_on) if legacy_available_on
|
|
75
|
+
unpublished += publications.where(unpublished_at: nil).update_all(unpublished_at: legacy_discontinue_on) if legacy_discontinue_on
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
total = published + unpublished
|
|
79
|
+
puts total.zero? ? ' All product-publication dates already populated.' : " Backfilled dates on #{published} published_at + #{unpublished} unpublished_at column(s)."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc 'Run the full 5.4 → 5.5 channel upgrade: create default channels, backfill products to store_id and publications, backfill order channels, backfill publication date windows'
|
|
83
|
+
task upgrade: [
|
|
84
|
+
:create_defaults,
|
|
85
|
+
'spree:upgrade:populate_publications',
|
|
86
|
+
:backfill_order_channel_ids,
|
|
87
|
+
:backfill_product_publication_dates
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
def legacy_channel_column?
|
|
91
|
+
ActiveRecord::Base.connection.column_exists?(:spree_orders, :channel)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/tasks/core.rake
CHANGED
data/lib/tasks/media.rake
CHANGED
|
@@ -16,5 +16,32 @@ namespace :spree do
|
|
|
16
16
|
|
|
17
17
|
puts 'Done!'
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
# Enqueues Spree::Media::MigrateProductAssetsJob for every product that
|
|
21
|
+
# still has at least one variant-pinned asset. The job is idempotent, so
|
|
22
|
+
# re-running this task is safe.
|
|
23
|
+
#
|
|
24
|
+
# ENV vars:
|
|
25
|
+
# BATCH_SIZE — products fetched per scope batch (default: 500)
|
|
26
|
+
desc 'Enqueue jobs to migrate legacy variant-pinned images to product-level media (opt-in, 5.5)'
|
|
27
|
+
task migrate_master_images_to_product_media: :environment do
|
|
28
|
+
batch_size = ENV.fetch('BATCH_SIZE', 500).to_i
|
|
29
|
+
batch_size = 500 if batch_size < 1
|
|
30
|
+
|
|
31
|
+
# Subquery (not pluck) so the product set doesn't materialize in Ruby —
|
|
32
|
+
# important for catalogs with millions of products.
|
|
33
|
+
variant_product_ids = Spree::Variant
|
|
34
|
+
.joins("INNER JOIN #{Spree::Asset.table_name} ON " \
|
|
35
|
+
"#{Spree::Asset.table_name}.viewable_id = #{Spree::Variant.table_name}.id " \
|
|
36
|
+
"AND #{Spree::Asset.table_name}.viewable_type = 'Spree::Variant'")
|
|
37
|
+
.select(:product_id)
|
|
38
|
+
|
|
39
|
+
relation = Spree::Product.where(id: variant_product_ids)
|
|
40
|
+
relation.find_each(batch_size: batch_size) do |product|
|
|
41
|
+
Spree::Media::MigrateProductAssetsJob.perform_later(product.id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "Enqueued migration jobs for #{relation.count} products on the #{Spree.queues.images} queue."
|
|
45
|
+
end
|
|
19
46
|
end
|
|
20
47
|
end
|
data/lib/tasks/products.rake
CHANGED
|
@@ -19,21 +19,19 @@ namespace :spree do
|
|
|
19
19
|
puts "\nDone!"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
desc 'Enqueue background jobs to populate product metrics for
|
|
22
|
+
desc 'Enqueue background jobs to populate product metrics for every product'
|
|
23
23
|
task populate_metrics: :environment do
|
|
24
24
|
total_count = 0
|
|
25
25
|
|
|
26
|
-
Spree::
|
|
27
|
-
jobs = batch.pluck(:
|
|
28
|
-
Spree::Products::RefreshMetricsJob.new(product_id, store_id)
|
|
29
|
-
end
|
|
26
|
+
Spree::Product.in_batches(of: 100) do |batch|
|
|
27
|
+
jobs = batch.pluck(:id).map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id) }
|
|
30
28
|
ActiveJob.perform_all_later(jobs)
|
|
31
29
|
total_count += jobs.size
|
|
32
30
|
print '.'
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
if total_count.zero?
|
|
36
|
-
puts 'No
|
|
34
|
+
puts 'No products found.'
|
|
37
35
|
else
|
|
38
36
|
puts "\nEnqueued #{total_count} jobs to refresh product metrics."
|
|
39
37
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
namespace :spree do
|
|
2
|
+
namespace :upgrade do
|
|
3
|
+
desc <<~DESC
|
|
4
|
+
Populates +spree_products.store_id+ and +spree_product_publications+ from the legacy
|
|
5
|
+
+spree_products_stores+ join. Idempotent — re-running skips products that
|
|
6
|
+
already have a +store_id+ and channels that already have a publication for
|
|
7
|
+
the product.
|
|
8
|
+
|
|
9
|
+
Run once after upgrading to Spree 5.5+. Multi-store merchants must install
|
|
10
|
+
+spree_multi_store+ before running; running on a multi-store catalog without
|
|
11
|
+
the extension picks the earliest +spree_products_stores+ row (by
|
|
12
|
+
+created_at+) as the product's home store.
|
|
13
|
+
DESC
|
|
14
|
+
task populate_publications: :environment do
|
|
15
|
+
unless ActiveRecord::Base.connection.table_exists?(Spree::StoreProduct.table_name)
|
|
16
|
+
puts " #{Spree::StoreProduct.table_name} table not found — nothing to migrate."
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
batch_size = (ENV['BATCH_SIZE'] || 1_000).to_i
|
|
21
|
+
publications_created = 0
|
|
22
|
+
|
|
23
|
+
# Pass 1: per store, batch-publish products onto the store's default
|
|
24
|
+
# channel via +Channel#add_products+. One upsert + one touch_all per
|
|
25
|
+
# batch beats the previous per-product loop by orders of magnitude on
|
|
26
|
+
# large catalogs. +add_products+ is upsert-based with +on_duplicate:
|
|
27
|
+
# :skip+, so existing publications on re-run are no-ops.
|
|
28
|
+
Spree::Store.find_each do |store|
|
|
29
|
+
channel = store.default_channel
|
|
30
|
+
unless channel
|
|
31
|
+
puts " Store '#{store.name}' has no default channel — skipping."
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
store_publications = 0
|
|
36
|
+
Spree::StoreProduct.where(store_id: store.id).in_batches(of: batch_size) do |batch|
|
|
37
|
+
store_publications += channel.add_products(batch.pluck(:product_id))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
publications_created += store_publications
|
|
41
|
+
puts " Store '#{store.name}': created #{store_publications} publication(s)" if store_publications.positive?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Pass 2: assign +store_id+ on products that still don't have one,
|
|
45
|
+
# using the earliest legacy row per product.
|
|
46
|
+
products_processed = 0
|
|
47
|
+
|
|
48
|
+
Spree::Product.where(store_id: nil).find_each(batch_size: batch_size) do |product|
|
|
49
|
+
store_id = Spree::StoreProduct.where(product_id: product.id).order(:created_at).limit(1).pick(:store_id)
|
|
50
|
+
next unless store_id
|
|
51
|
+
|
|
52
|
+
product.update_column(:store_id, store_id)
|
|
53
|
+
products_processed += 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts " Processed #{products_processed} products"
|
|
57
|
+
puts " Created #{publications_created} publications"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|