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
|
@@ -16,19 +16,6 @@ module Spree
|
|
|
16
16
|
validates :variant, uniqueness: { scope: [:wishlist] }
|
|
17
17
|
validates :quantity, numericality: { only_integer: true, greater_than: 0 }
|
|
18
18
|
|
|
19
|
-
# This is a workaround to allow the variant_id to be set with a prefixed ID
|
|
20
|
-
# in the API.
|
|
21
|
-
#
|
|
22
|
-
# @param id [String] the prefixed ID of the variant
|
|
23
|
-
def variant_id=(id)
|
|
24
|
-
if id.to_s.include?('_')
|
|
25
|
-
decoded = Spree::Variant.decode_prefixed_id(id)
|
|
26
|
-
super(decoded)
|
|
27
|
-
else
|
|
28
|
-
super(id)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
19
|
def price(currency)
|
|
33
20
|
variant.amount_in(currency[:currency])
|
|
34
21
|
end
|
|
@@ -53,7 +53,7 @@ module Spree
|
|
|
53
53
|
@index = index
|
|
54
54
|
@properties = properties
|
|
55
55
|
@taxons = taxons
|
|
56
|
-
@store = store || product.
|
|
56
|
+
@store = store || product.store
|
|
57
57
|
@currency = currency || @store.default_currency
|
|
58
58
|
@price_only = @currency != @store.default_currency
|
|
59
59
|
@metafields = metafields
|
|
@@ -100,8 +100,8 @@ module Spree
|
|
|
100
100
|
variant.dimensions_unit,
|
|
101
101
|
variant.weight,
|
|
102
102
|
variant.weight_unit,
|
|
103
|
-
|
|
104
|
-
(variant.discontinue_on ||
|
|
103
|
+
publication_available_on&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
104
|
+
(variant.discontinue_on || publication_discontinue_on)&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
105
105
|
variant.track_inventory?,
|
|
106
106
|
total_on_hand == BigDecimal::INFINITY ? '∞' : total_on_hand,
|
|
107
107
|
variant.backorderable?,
|
|
@@ -137,6 +137,26 @@ module Spree
|
|
|
137
137
|
|
|
138
138
|
private
|
|
139
139
|
|
|
140
|
+
# Default-channel publication for the export's store. 5.5 transitional:
|
|
141
|
+
# fall back to the legacy Product columns when the publication dates are
|
|
142
|
+
# NULL (pre-backfill). 6.0 drops the Product-column fallback.
|
|
143
|
+
def default_publication
|
|
144
|
+
return @default_publication if defined?(@default_publication)
|
|
145
|
+
|
|
146
|
+
channel_id = store&.default_channel&.id
|
|
147
|
+
@default_publication = channel_id && product.product_publications.find do |p|
|
|
148
|
+
p.store_id == store.id && p.channel_id == channel_id
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def publication_available_on
|
|
153
|
+
default_publication&.published_at || product.available_on
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def publication_discontinue_on
|
|
157
|
+
default_publication&.unpublished_at || product.discontinue_on
|
|
158
|
+
end
|
|
159
|
+
|
|
140
160
|
def price_only_row
|
|
141
161
|
csv = Array.new(CSV_HEADERS.size)
|
|
142
162
|
csv[CSV_HEADERS.index('sku')] = variant.sku
|
|
@@ -58,7 +58,8 @@ module Spree
|
|
|
58
58
|
status: product.status,
|
|
59
59
|
sku: product.sku,
|
|
60
60
|
in_stock: product.in_stock?,
|
|
61
|
-
store_ids: product.
|
|
61
|
+
store_ids: Array(product.store_id).map(&:to_s),
|
|
62
|
+
channel_ids: channel_ids_for_store,
|
|
62
63
|
discontinue_on: product.discontinue_on&.to_i || 0,
|
|
63
64
|
category_ids: category_ids_with_ancestors,
|
|
64
65
|
category_names: product.taxons.map { |t| translated(t, :name, fallback_locale) },
|
|
@@ -67,7 +68,7 @@ module Spree
|
|
|
67
68
|
option_value_ids: variant_option_value_ids,
|
|
68
69
|
option_values: variant_option_values_data.map { |ov| translated(ov, :presentation, fallback_locale) }.uniq,
|
|
69
70
|
tags: product.tag_list || [],
|
|
70
|
-
units_sold_count: product.
|
|
71
|
+
units_sold_count: product.units_sold_count || 0,
|
|
71
72
|
available_on: product.available_on&.iso8601,
|
|
72
73
|
created_at: product.created_at&.iso8601,
|
|
73
74
|
updated_at: product.updated_at&.iso8601
|
|
@@ -107,8 +108,14 @@ module Spree
|
|
|
107
108
|
@compare_at_cache[currency]
|
|
108
109
|
end
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
def channel_ids_for_store
|
|
112
|
+
@channel_ids_for_store ||= product.product_publications
|
|
113
|
+
.joins(:channel)
|
|
114
|
+
.where(spree_channels: { store_id: store.id })
|
|
115
|
+
.pluck(:channel_id)
|
|
116
|
+
.map(&:to_s)
|
|
117
|
+
end
|
|
118
|
+
|
|
112
119
|
def category_ids_with_ancestors
|
|
113
120
|
@category_ids_with_ancestors ||= product.taxons.flat_map { |t|
|
|
114
121
|
t.self_and_ancestors.map(&:prefixed_id)
|
|
@@ -16,10 +16,11 @@ module Spree
|
|
|
16
16
|
|
|
17
17
|
def call
|
|
18
18
|
@variants.map do |variant|
|
|
19
|
+
price = variant.price_in(current_currency)
|
|
19
20
|
{
|
|
20
|
-
display_price:
|
|
21
|
-
price:
|
|
22
|
-
display_compare_at_price:
|
|
21
|
+
display_price: price.display_price_including_vat_for(current_price_options).to_html,
|
|
22
|
+
price: price,
|
|
23
|
+
display_compare_at_price: price.display_compare_at_price_including_vat_for(current_price_options).to_html,
|
|
23
24
|
should_display_compare_at_price: should_display_compare_at_price?(variant),
|
|
24
25
|
is_product_available_in_currency: @is_product_available_in_currency,
|
|
25
26
|
backorderable: backorderable?(variant),
|
|
@@ -11,8 +11,6 @@ module Spree
|
|
|
11
11
|
default_billing = address_params.key?(:is_default_billing) ? address_params.delete(:is_default_billing) : opts.fetch(:default_billing, false)
|
|
12
12
|
default_shipping = address_params.key?(:is_default_shipping) ? address_params.delete(:is_default_shipping) : opts.fetch(:default_shipping, false)
|
|
13
13
|
address_changes_except = opts.fetch(:address_changes_except, [])
|
|
14
|
-
create_new_address_on_update = opts.fetch(:create_new_address_on_update, false)
|
|
15
|
-
Spree::Deprecation.warn('Spree::Addresses::Update create_new_address_on_update parameter is deprecated and will be removed in Spree 5.5.') if create_new_address_on_update
|
|
16
14
|
|
|
17
15
|
prepare_address_params!(address, address_params)
|
|
18
16
|
address.assign_attributes(address_params)
|
|
@@ -36,7 +34,7 @@ module Spree
|
|
|
36
34
|
|
|
37
35
|
return success(address) unless address_changed
|
|
38
36
|
|
|
39
|
-
if address.editable?
|
|
37
|
+
if address.editable?
|
|
40
38
|
if address.update(address_params)
|
|
41
39
|
if address.user.present?
|
|
42
40
|
assign_to_user_as_default(
|
|
@@ -54,11 +52,11 @@ module Spree
|
|
|
54
52
|
failure(address)
|
|
55
53
|
end
|
|
56
54
|
elsif new_address(address_params).valid?
|
|
57
|
-
address.destroy
|
|
55
|
+
address.destroy
|
|
58
56
|
|
|
59
57
|
if new_address.user.present?
|
|
60
|
-
default_billing =
|
|
61
|
-
default_shipping =
|
|
58
|
+
default_billing = address.user_default_billing? || default_billing
|
|
59
|
+
default_shipping = address.user_default_shipping? || default_shipping
|
|
62
60
|
|
|
63
61
|
assign_to_user_as_default(
|
|
64
62
|
user: new_address.user,
|
|
@@ -69,8 +67,8 @@ module Spree
|
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
if order.present?
|
|
72
|
-
order.ship_address = new_address if
|
|
73
|
-
order.bill_address = new_address if
|
|
70
|
+
order.ship_address = new_address if order.ship_address_id == address.id
|
|
71
|
+
order.bill_address = new_address if order.bill_address_id == address.id
|
|
74
72
|
order.state = 'address'
|
|
75
73
|
order.save
|
|
76
74
|
end
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
def call(order:, variant:, quantity: nil, metadata: {}, public_metadata: {}, private_metadata: {}, options: {})
|
|
7
7
|
ApplicationRecord.transaction do
|
|
8
8
|
run :add_to_line_item
|
|
9
|
+
run :handle_stock_reservations
|
|
9
10
|
run Spree.cart_recalculate_service
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -48,6 +49,15 @@ module Spree
|
|
|
48
49
|
::Spree::TaxRate.adjust(order, [line_item]) if line_item_created
|
|
49
50
|
success(order: order, line_item: line_item, line_item_created: line_item_created, options: options)
|
|
50
51
|
end
|
|
52
|
+
|
|
53
|
+
def handle_stock_reservations(order:, line_item:, line_item_created:, options:)
|
|
54
|
+
if order.in_checkout?
|
|
55
|
+
result = Spree::StockReservations::Reserve.call(order: order)
|
|
56
|
+
return failure(line_item, result.error) if result.failure?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
success(order: order, line_item: line_item, line_item_created: line_item_created, options: options)
|
|
60
|
+
end
|
|
51
61
|
end
|
|
52
62
|
end
|
|
53
63
|
end
|
|
@@ -7,11 +7,21 @@ module Spree
|
|
|
7
7
|
options ||= {}
|
|
8
8
|
ActiveRecord::Base.transaction do
|
|
9
9
|
order.line_items.destroy(line_item)
|
|
10
|
+
|
|
11
|
+
# LineItem dependent: :destroy removes its own reservation row;
|
|
12
|
+
# remaining items may need a fresh reservation pass when in checkout.
|
|
13
|
+
if order.in_checkout? && order.line_items.any?
|
|
14
|
+
result = Spree::StockReservations::Reserve.call(order: order)
|
|
15
|
+
raise Spree::StockReservations::InsufficientStockError.new(nil, result.error.to_s) if result.failure?
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
Spree.cart_recalculate_service.new.call(order: order,
|
|
11
19
|
line_item: line_item,
|
|
12
20
|
options: options)
|
|
13
21
|
end
|
|
14
22
|
success(line_item)
|
|
23
|
+
rescue Spree::StockReservations::InsufficientStockError => e
|
|
24
|
+
failure(line_item, e.message)
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
|
|
10
10
|
return success([order, @messages, @warnings]) if order.item_count.zero? || order.line_items.none?
|
|
11
11
|
|
|
12
|
-
line_items = order.line_items.includes(variant: [:product, :
|
|
12
|
+
line_items = order.line_items.includes(variant: [:product, :stock_locations, { stock_items: [:stock_location, :active_stock_reservations] }])
|
|
13
13
|
|
|
14
14
|
ActiveRecord::Base.transaction do
|
|
15
15
|
line_items.each do |line_item|
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
def call(order:, line_item:, quantity: nil)
|
|
7
7
|
ActiveRecord::Base.transaction do
|
|
8
8
|
run :change_item_quantity
|
|
9
|
+
run :handle_stock_reservations
|
|
9
10
|
run Spree.cart_recalculate_service
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -17,6 +18,15 @@ module Spree
|
|
|
17
18
|
|
|
18
19
|
success(order: order, line_item: line_item)
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
def handle_stock_reservations(order:, line_item:)
|
|
23
|
+
if order.in_checkout?
|
|
24
|
+
result = Spree::StockReservations::Reserve.call(order: order)
|
|
25
|
+
return failure(line_item, result.error) if result.failure?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
success(order: order, line_item: line_item)
|
|
29
|
+
end
|
|
20
30
|
end
|
|
21
31
|
end
|
|
22
32
|
end
|
|
@@ -12,6 +12,7 @@ module Spree
|
|
|
12
12
|
cart = store.carts.create!(
|
|
13
13
|
user: @params.delete(:user),
|
|
14
14
|
market: @params.delete(:market) || Spree::Current.market,
|
|
15
|
+
channel: @params.delete(:channel) || Spree::Current.channel,
|
|
15
16
|
currency: @params.delete(:currency) || store.default_currency,
|
|
16
17
|
locale: @params.delete(:locale) || Spree::Current.locale
|
|
17
18
|
)
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
def call(cart:, params:)
|
|
7
7
|
@cart = cart
|
|
8
8
|
@params = params.to_h.deep_symbolize_keys
|
|
9
|
+
was_in_cart = cart.cart?
|
|
9
10
|
|
|
10
11
|
ApplicationRecord.transaction do
|
|
11
12
|
assign_cart_attributes
|
|
@@ -16,10 +17,10 @@ module Spree
|
|
|
16
17
|
cart.save!
|
|
17
18
|
|
|
18
19
|
process_items
|
|
20
|
+
try_advance
|
|
21
|
+
sync_stock_reservations(was_in_cart: was_in_cart)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
|
-
try_advance
|
|
22
|
-
|
|
23
24
|
success(cart)
|
|
24
25
|
rescue ActiveRecord::RecordNotFound
|
|
25
26
|
raise
|
|
@@ -110,6 +111,21 @@ module Spree
|
|
|
110
111
|
cart.state = 'address'
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
# Three-way dispatch on the cart→checkout transition:
|
|
115
|
+
# entering checkout → Reserve, mid-checkout mutation → Extend, reverting to cart → Release.
|
|
116
|
+
# A failed Reserve raises so the enclosing transaction rolls back and the
|
|
117
|
+
# outer rescue surfaces the error to the API caller.
|
|
118
|
+
def sync_stock_reservations(was_in_cart:)
|
|
119
|
+
if cart.cart?
|
|
120
|
+
Spree::StockReservations::Release.call(order: cart) unless was_in_cart
|
|
121
|
+
elsif was_in_cart
|
|
122
|
+
result = Spree::StockReservations::Reserve.call(order: cart)
|
|
123
|
+
raise Spree::StockReservations::InsufficientStockError.new(nil, result.error.to_s) if result.failure?
|
|
124
|
+
else
|
|
125
|
+
Spree::StockReservations::Extend.call(order: cart)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
113
129
|
# Auto-advance as far as the checkout state machine allows, but never
|
|
114
130
|
# to complete. The complete transition must always be explicit via
|
|
115
131
|
# the /carts/:id/complete endpoint — otherwise gift cards or store
|
|
@@ -57,16 +57,16 @@ module Spree
|
|
|
57
57
|
|
|
58
58
|
private
|
|
59
59
|
|
|
60
|
-
def resolve_variant(store,
|
|
61
|
-
return nil if
|
|
60
|
+
def resolve_variant(store, variant_id)
|
|
61
|
+
return nil if variant_id.blank?
|
|
62
62
|
|
|
63
|
-
variant = store.variants.
|
|
63
|
+
variant = store.variants.find_by_param(variant_id)
|
|
64
64
|
|
|
65
65
|
raise ActiveRecord::RecordNotFound.new(
|
|
66
|
-
"Variant '#{
|
|
66
|
+
"Variant '#{variant_id}' not found in this store",
|
|
67
67
|
'Spree::Variant',
|
|
68
|
-
'
|
|
69
|
-
|
|
68
|
+
'id',
|
|
69
|
+
variant_id
|
|
70
70
|
) unless variant
|
|
71
71
|
|
|
72
72
|
variant
|
|
@@ -34,7 +34,10 @@ module Spree
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def assign_address(user)
|
|
37
|
-
|
|
37
|
+
# Save user first so the address can FK to it via user_id (has_many :addresses).
|
|
38
|
+
user.save! if user.new_record?
|
|
39
|
+
|
|
40
|
+
address = user.bill_address || user.addresses.build
|
|
38
41
|
address.firstname = attributes['first_name'].presence || user.first_name
|
|
39
42
|
address.lastname = attributes['last_name'].presence || user.last_name
|
|
40
43
|
address.company = attributes['company'].strip if attributes['company'].present?
|
|
@@ -66,9 +66,18 @@ module Spree
|
|
|
66
66
|
product = existing_product if existing_product.present?
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
# Store is touched when the import completes
|
|
70
|
+
Spree::Store.no_touching do
|
|
71
|
+
product = assign_attributes_to_product(product)
|
|
72
|
+
product.save!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
handle_tags(product) if attributes['tags'].present?
|
|
76
|
+
if has_product_attributes?
|
|
77
|
+
handle_metafields(product)
|
|
78
|
+
handle_categories(product)
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
product
|
|
73
82
|
else
|
|
74
83
|
# For non-master variants, only look up the product
|
|
@@ -89,27 +98,21 @@ module Spree
|
|
|
89
98
|
if product.new_record?
|
|
90
99
|
product.slug = attributes['slug']
|
|
91
100
|
product.sku = attributes['sku'] if attributes['sku'].present? && options.empty?
|
|
101
|
+
product.store = store
|
|
92
102
|
end
|
|
93
103
|
|
|
94
|
-
product.stores << store if product.stores.exclude?(store)
|
|
95
104
|
product.name = attributes['name'] if attributes['name'].present?
|
|
96
105
|
product.description = attributes['description'] if attributes['description'].present?
|
|
97
106
|
product.meta_title = attributes['meta_title'] if attributes['meta_title'].present?
|
|
98
107
|
product.meta_description = attributes['meta_description'] if attributes['meta_description'].present?
|
|
99
108
|
product.meta_keywords = attributes['meta_keywords'] if attributes['meta_keywords'].present?
|
|
100
109
|
product.status = to_spree_status(attributes['status']) if attributes['status'].present?
|
|
101
|
-
product.tag_list = attributes['tags'] if attributes['tags'].present?
|
|
102
110
|
|
|
103
111
|
if options.empty?
|
|
104
112
|
if attributes['shipping_category'].present?
|
|
105
113
|
shipping_category = prepare_shipping_category
|
|
106
114
|
product.shipping_category = shipping_category if shipping_category.present?
|
|
107
115
|
end
|
|
108
|
-
|
|
109
|
-
taxons = prepare_taxons
|
|
110
|
-
# Full product rows (with name/status/description) clear taxons when categories are blank.
|
|
111
|
-
# Price-only rows (no product attributes) never touch taxons.
|
|
112
|
-
product.taxons = taxons if taxons.any? || has_product_attributes?
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
product
|
|
@@ -125,47 +128,73 @@ module Spree
|
|
|
125
128
|
Spree::TaxCategory.find_by(name: tax_category_name)
|
|
126
129
|
end
|
|
127
130
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
attributes['category1'],
|
|
131
|
-
attributes['category2'],
|
|
132
|
-
attributes['category3']
|
|
133
|
-
].compact_blank.map(&:strip).uniq
|
|
131
|
+
def prepare_option_value_variants
|
|
132
|
+
return [] if options.empty?
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
ActiveRecord::Base.no_touching do
|
|
135
|
+
options.map do |option|
|
|
136
|
+
option_type = find_or_create_option_type!(option[:option_name])
|
|
137
|
+
option_value = find_or_create_option_value!(option_type, option[:option_value])
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
# ensure product option types include new option type
|
|
140
|
+
find_or_create_product_option_type!(option_type)
|
|
141
|
+
|
|
142
|
+
Spree::OptionValueVariant.new(option_value: option_value)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
139
145
|
end
|
|
140
146
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
def options
|
|
148
|
+
@options ||= begin
|
|
149
|
+
options = []
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
OPTION_TYPES_COUNT.times.map do |index|
|
|
152
|
+
next if attributes["option#{index + 1}_name"].blank?
|
|
153
|
+
next if attributes["option#{index + 1}_value"].blank?
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
options << {
|
|
156
|
+
index: index + 1,
|
|
157
|
+
option_name: attributes["option#{index + 1}_name"],
|
|
158
|
+
option_value: attributes["option#{index + 1}_value"]
|
|
159
|
+
}
|
|
160
|
+
end
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
last_taxon = taxonomy.taxons.with_matching_name(taxon_name).where(parent: last_taxon).first || taxonomy.taxons.create!(name: taxon_name, parent: last_taxon)
|
|
162
|
+
options
|
|
152
163
|
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Concurrent CSV imports can race when creating shared OptionTypes/OptionValues.
|
|
167
|
+
# Recover the losing worker by re-fetching the peer's row whether the conflict
|
|
168
|
+
# surfaces via the DB unique index (RecordNotUnique) or the AR uniqueness
|
|
169
|
+
# validator (RecordInvalid with a :taken error on the relevant attribute).
|
|
170
|
+
def find_or_create_option_type!(presentation)
|
|
171
|
+
Spree::OptionType.search_by_name(presentation).first || Spree::OptionType.create!(presentation: presentation)
|
|
172
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
173
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
Spree::OptionType.search_by_name(presentation).first!
|
|
155
176
|
end
|
|
156
177
|
|
|
157
|
-
def
|
|
158
|
-
|
|
178
|
+
def find_or_create_option_value!(option_type, presentation)
|
|
179
|
+
option_type.option_values.search_by_name(presentation).first || option_type.option_values.create!(presentation: presentation)
|
|
180
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
181
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
159
182
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
option_value = option_type.option_values.search_by_name(option[:option_value]).first || option_type.option_values.create!(presentation: option[:option_value])
|
|
183
|
+
option_type.option_values.search_by_name(presentation).first!
|
|
184
|
+
end
|
|
163
185
|
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
def find_or_create_product_option_type!(option_type)
|
|
187
|
+
Spree::ProductOptionType.find_or_create_by!(product: product, option_type: option_type)
|
|
188
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
189
|
+
raise unless uniqueness_conflict?(e, :product_id)
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
Spree::ProductOptionType.find_by!(product: product, option_type: option_type)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# RecordNotUnique is always a uniqueness conflict; RecordInvalid only when the
|
|
195
|
+
# given attribute has a :taken error (other validation failures must propagate).
|
|
196
|
+
def uniqueness_conflict?(error, attribute)
|
|
197
|
+
error.is_a?(ActiveRecord::RecordNotUnique) || error.record.errors.where(attribute, :taken).any?
|
|
169
198
|
end
|
|
170
199
|
|
|
171
200
|
def handle_images(variant)
|
|
@@ -177,27 +206,20 @@ module Spree
|
|
|
177
206
|
|
|
178
207
|
return if image_urls.empty?
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def options
|
|
186
|
-
@options ||= begin
|
|
187
|
-
options = []
|
|
209
|
+
# Always attach to the product so blobs aren't duplicated across
|
|
210
|
+
# variants. For non-master rows, pass the variant id so the job links
|
|
211
|
+
# the resulting product-level asset to that variant via VariantMedia.
|
|
212
|
+
link_variant_id = variant.is_master? ? nil : variant.id
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
options
|
|
214
|
+
image_urls.each do |image_url|
|
|
215
|
+
Spree::Images::SaveFromUrlJob.perform_later(
|
|
216
|
+
product.id,
|
|
217
|
+
'Spree::Product',
|
|
218
|
+
image_url,
|
|
219
|
+
nil,
|
|
220
|
+
nil,
|
|
221
|
+
link_variant_id
|
|
222
|
+
)
|
|
201
223
|
end
|
|
202
224
|
end
|
|
203
225
|
|
|
@@ -251,6 +273,22 @@ module Spree
|
|
|
251
273
|
product.update(metafields_attributes: nested_attrs) unless nested_attrs.empty?
|
|
252
274
|
end
|
|
253
275
|
|
|
276
|
+
def handle_tags(product)
|
|
277
|
+
Spree::Imports::AssignTagsJob.perform_later(product.id, attributes['tags'])
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def handle_categories(product)
|
|
281
|
+
Spree::Imports::CreateCategoriesJob.perform_later(product.id, store.id, prepare_taxon_pretty_names)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def prepare_taxon_pretty_names
|
|
285
|
+
[
|
|
286
|
+
attributes['category1'],
|
|
287
|
+
attributes['category2'],
|
|
288
|
+
attributes['category3']
|
|
289
|
+
].compact_blank.map(&:strip).uniq
|
|
290
|
+
end
|
|
291
|
+
|
|
254
292
|
def to_spree_status(status)
|
|
255
293
|
case status.strip.downcase
|
|
256
294
|
when 'active'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Newsletter
|
|
3
|
+
# Reconciles a Spree::NewsletterSubscriber with the customer who owns the email.
|
|
4
|
+
# Backfills the user link and propagates verified opt-in onto the user record so
|
|
5
|
+
# consent given before account creation isn't silently lost on registration.
|
|
6
|
+
#
|
|
7
|
+
# Best-effort: validation failures are logged but never re-raised. Callers are
|
|
8
|
+
# already past the point where rolling back makes sense (the user record exists,
|
|
9
|
+
# the subscription exists). This is reconciliation, not a precondition.
|
|
10
|
+
class LinkUser
|
|
11
|
+
def initialize(subscriber:, user:)
|
|
12
|
+
@subscriber = subscriber
|
|
13
|
+
@user = user
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return if subscriber.blank? || user.blank?
|
|
18
|
+
return if subscriber.user_id == user.id && !needs_marketing_propagation?
|
|
19
|
+
|
|
20
|
+
link_subscriber_to_user
|
|
21
|
+
propagate_marketing_consent if needs_marketing_propagation?
|
|
22
|
+
|
|
23
|
+
subscriber
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :subscriber, :user
|
|
29
|
+
|
|
30
|
+
def link_subscriber_to_user
|
|
31
|
+
return if subscriber.user_id == user.id
|
|
32
|
+
|
|
33
|
+
return if subscriber.update(user: user)
|
|
34
|
+
|
|
35
|
+
Rails.logger.warn(
|
|
36
|
+
"NewsletterSubscriber #{subscriber.id} link to user #{user.id} failed: #{subscriber.errors.full_messages.to_sentence}"
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def needs_marketing_propagation?
|
|
41
|
+
subscriber.verified? && !user.accepts_email_marketing?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def propagate_marketing_consent
|
|
45
|
+
return if user.update(accepts_email_marketing: true)
|
|
46
|
+
|
|
47
|
+
Rails.logger.warn(
|
|
48
|
+
"User #{user.id} accepts_email_marketing update failed: #{user.errors.full_messages.to_sentence}"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|