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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Product
|
|
3
|
+
module Channels
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
DEPRECATED_DATE_TO_PUBLICATION_FIELD = {
|
|
7
|
+
available_on: :published_at,
|
|
8
|
+
discontinue_on: :unpublished_at
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
belongs_to :store, class_name: 'Spree::Store', optional: true
|
|
13
|
+
|
|
14
|
+
# No +dependent: :destroy+: Product uses +acts_as_paranoid+, so destroy
|
|
15
|
+
# soft-deletes and publications outlive the product.
|
|
16
|
+
# +inverse_of: :product+ is what wires the parent into a child built off
|
|
17
|
+
# an unsaved Product (via +accepts_nested_attributes_for+ on the legacy
|
|
18
|
+
# alias below). Without it the child fails +validates :product,
|
|
19
|
+
# presence: true+ on +create+. Declared on both associations to keep
|
|
20
|
+
# the two in-memory caches symmetric (V-3454).
|
|
21
|
+
has_many :product_publications, class_name: 'Spree::ProductPublication',
|
|
22
|
+
inverse_of: :product, autosave: true
|
|
23
|
+
has_many :channels, -> { distinct }, through: :product_publications, class_name: 'Spree::Channel'
|
|
24
|
+
|
|
25
|
+
# Legacy Rails admin alias. The admin form submits
|
|
26
|
+
# +legacy_product_publications_attributes+ (with +_destroy+ flags and
|
|
27
|
+
# +reject_if+ semantics); the v3 API submits +product_publications+
|
|
28
|
+
# and goes through the custom writer below. Two names, one table —
|
|
29
|
+
# no +dependent:+ for the same +acts_as_paranoid+ reason as above.
|
|
30
|
+
has_many :legacy_product_publications, class_name: 'Spree::ProductPublication',
|
|
31
|
+
foreign_key: :product_id,
|
|
32
|
+
inverse_of: :product, autosave: true
|
|
33
|
+
accepts_nested_attributes_for :legacy_product_publications,
|
|
34
|
+
allow_destroy: true,
|
|
35
|
+
reject_if: ->(attrs) { attrs[:channel_id].blank? }
|
|
36
|
+
|
|
37
|
+
before_validation :assign_default_store, if: -> { store.nil? }
|
|
38
|
+
after_create :apply_pending_publications, if: :pending_publications?
|
|
39
|
+
|
|
40
|
+
DEPRECATED_DATE_TO_PUBLICATION_FIELD.each do |legacy_attr, publication_attr|
|
|
41
|
+
define_method("#{legacy_attr}=") do |value|
|
|
42
|
+
Spree::Deprecation.warn(
|
|
43
|
+
"Spree::Product##{legacy_attr}= is deprecated; set #{publication_attr} on " \
|
|
44
|
+
"ProductPublication instead (writes to every channel's publication). "
|
|
45
|
+
)
|
|
46
|
+
super(value)
|
|
47
|
+
product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reading +available_on+/+discontinue_on+ prefers the current-channel
|
|
51
|
+
# publication's date and falls back to the legacy Product column
|
|
52
|
+
# whenever the publication's value is nil. This 5.5 transition
|
|
53
|
+
# behavior is dropped in 6.0 when the legacy columns are removed.
|
|
54
|
+
define_method(legacy_attr) do
|
|
55
|
+
channel = Spree::Current.channel
|
|
56
|
+
publication = channel && publication_for(channel)
|
|
57
|
+
(publication && publication.public_send(publication_attr)) || super()
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the publication for the given channel, or nil if the product isn't published there.
|
|
63
|
+
# @param channel [Spree::Channel] the channel to find the publication for
|
|
64
|
+
# @return [Spree::ProductPublication, nil] the publication for the channel, or nil if not published
|
|
65
|
+
def publication_for(channel)
|
|
66
|
+
return nil unless channel
|
|
67
|
+
|
|
68
|
+
if product_publications.loaded?
|
|
69
|
+
product_publications.find { |p| p.channel_id == channel.id }
|
|
70
|
+
else
|
|
71
|
+
product_publications.find_by(channel_id: channel.id)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Syncs product publications from an array of hashes.
|
|
76
|
+
# Creates new publications, updates existing ones (matched by +:id+ or
|
|
77
|
+
# +:channel_id+), and removes ones absent from the payload. An empty
|
|
78
|
+
# array detaches the product from every channel.
|
|
79
|
+
# @param publications_params [Array<Hash>] array of publication attribute hashes
|
|
80
|
+
# @return [void]
|
|
81
|
+
def product_publications=(publications_params)
|
|
82
|
+
return super if publications_params.nil?
|
|
83
|
+
return super if publications_params.respond_to?(:first) && publications_params.first.is_a?(Spree::ProductPublication)
|
|
84
|
+
|
|
85
|
+
if new_record?
|
|
86
|
+
@pending_publications_params = publications_params
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
apply_product_publications(publications_params)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def assign_default_store
|
|
96
|
+
self.store ||= Spree::Current.store || Spree::Store.default
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pending_publications?
|
|
100
|
+
@pending_publications_params.present?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply_pending_publications
|
|
104
|
+
return unless @pending_publications_params
|
|
105
|
+
|
|
106
|
+
apply_product_publications(@pending_publications_params)
|
|
107
|
+
@pending_publications_params = nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_product_publications(publications_params)
|
|
111
|
+
publication_ids_in_payload = []
|
|
112
|
+
|
|
113
|
+
publications_params.each do |publication_data|
|
|
114
|
+
publication_data = publication_data.to_h.with_indifferent_access
|
|
115
|
+
publication_id = publication_data.delete(:id)
|
|
116
|
+
channel_id = decode_publication_channel_id(publication_data[:channel_id])
|
|
117
|
+
|
|
118
|
+
if publication_id.present?
|
|
119
|
+
decoded_id = Spree::PrefixedId.prefixed_id?(publication_id) ?
|
|
120
|
+
Spree::PrefixedId.decode_prefixed_id(publication_id) :
|
|
121
|
+
publication_id
|
|
122
|
+
publication = product_publications.find_by(id: decoded_id)
|
|
123
|
+
next unless publication
|
|
124
|
+
|
|
125
|
+
# Channel is immutable; ignore any rebind attempt.
|
|
126
|
+
publication.update!(publication_data.slice(:published_at, :unpublished_at))
|
|
127
|
+
publication_ids_in_payload << publication.id
|
|
128
|
+
elsif channel_id.present?
|
|
129
|
+
# Upsert by channel_id so repeat submissions are idempotent
|
|
130
|
+
# against the unique (product_id, channel_id) index.
|
|
131
|
+
publication = product_publications.find_or_initialize_by(channel_id: channel_id)
|
|
132
|
+
publication.assign_attributes(publication_data.slice(:published_at, :unpublished_at))
|
|
133
|
+
publication.save!
|
|
134
|
+
publication_ids_in_payload << publication.id
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
product_publications.where.not(id: publication_ids_in_payload).destroy_all
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def decode_publication_channel_id(value)
|
|
142
|
+
return nil if value.blank?
|
|
143
|
+
return value unless Spree::PrefixedId.prefixed_id?(value)
|
|
144
|
+
|
|
145
|
+
Spree::PrefixedId.decode_prefixed_id(value) || value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Product
|
|
3
|
+
# Legacy multi-store support. In Spree 5.5+ the +Spree::ProductPublication+ model
|
|
4
|
+
# handles the Product↔Store relation, so this module only provides a fallback
|
|
5
|
+
# for legacy code that still references +Spree::Product#stores+.
|
|
6
|
+
module LegacyMultiStoreSupport
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
def stores
|
|
11
|
+
Spree::Deprecation.warn(
|
|
12
|
+
"Spree::Product#stores is deprecated. Please use Spree::Product.store instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
13
|
+
)
|
|
14
|
+
store ? [store] : []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def store_ids
|
|
18
|
+
Spree::Deprecation.warn(
|
|
19
|
+
"Spree::Product#store_ids is deprecated. Please use Spree::Product.store_id instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
20
|
+
)
|
|
21
|
+
store_id ? [store_id] : []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stores=(stores)
|
|
25
|
+
Spree::Deprecation.warn(
|
|
26
|
+
"Spree::Product#stores= is deprecated. Please use Spree::Product.store= instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
27
|
+
)
|
|
28
|
+
self.store = Array(stores).compact.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def store_ids=(store_ids)
|
|
32
|
+
Spree::Deprecation.warn(
|
|
33
|
+
"Spree::Product#store_ids= is deprecated. Please use Spree::Product.store_id= instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
34
|
+
)
|
|
35
|
+
self.store_id = Array(store_ids).compact_blank.first
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/app/models/spree/product.rb
CHANGED
|
@@ -27,13 +27,14 @@ module Spree
|
|
|
27
27
|
normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
|
|
28
28
|
|
|
29
29
|
include Spree::ProductScopes
|
|
30
|
-
include Spree::StoreScopedResource
|
|
31
30
|
include Spree::TranslatableResource
|
|
32
31
|
include Spree::MemoizedData
|
|
33
32
|
include Spree::Metafields
|
|
34
33
|
include Spree::Metadata
|
|
35
34
|
include Spree::Product::Webhooks
|
|
36
35
|
include Spree::Product::Slugs
|
|
36
|
+
include Spree::Product::Channels
|
|
37
|
+
include Spree::Product::LegacyMultiStoreSupport unless defined?(SpreeMultiStore)
|
|
37
38
|
include Spree::SearchIndexable
|
|
38
39
|
if defined?(Spree::VendorConcern)
|
|
39
40
|
include Spree::VendorConcern
|
|
@@ -121,8 +122,6 @@ module Spree
|
|
|
121
122
|
|
|
122
123
|
has_many :prices_including_master, -> { non_zero }, through: :variants_including_master, source: :prices
|
|
123
124
|
|
|
124
|
-
has_many :store_products, class_name: 'Spree::StoreProduct'
|
|
125
|
-
has_many :stores, through: :store_products, class_name: 'Spree::Store'
|
|
126
125
|
has_many :digitals, through: :variants_including_master
|
|
127
126
|
|
|
128
127
|
after_initialize :ensure_master
|
|
@@ -133,6 +132,8 @@ module Spree
|
|
|
133
132
|
|
|
134
133
|
after_create :add_associations_from_prototype
|
|
135
134
|
after_create :build_variants_from_option_values_hash, if: :option_values_hash
|
|
135
|
+
after_create :apply_pending_variants, if: :pending_variants?
|
|
136
|
+
after_save :apply_pending_media, if: :pending_media?
|
|
136
137
|
|
|
137
138
|
after_save :save_master
|
|
138
139
|
after_save :run_touch_callbacks, if: :anything_changed?
|
|
@@ -153,7 +154,7 @@ module Spree
|
|
|
153
154
|
|
|
154
155
|
validate :discontinue_on_must_be_later_than_make_active_at, if: -> { make_active_at && discontinue_on }
|
|
155
156
|
|
|
156
|
-
scope :for_store, ->(store) {
|
|
157
|
+
scope :for_store, ->(store) { where(store_id: store.id) }
|
|
157
158
|
scope :draft, -> { where(status: 'draft') }
|
|
158
159
|
scope :archived, -> { where(status: 'archived') }
|
|
159
160
|
scope :not_archived, -> { where.not(status: 'archived') }
|
|
@@ -197,8 +198,71 @@ module Spree
|
|
|
197
198
|
|
|
198
199
|
alias options product_option_types
|
|
199
200
|
|
|
201
|
+
# Maps tags array to tag_list for API convenience.
|
|
202
|
+
# @param tags [Array<String>]
|
|
203
|
+
def tags=(tags)
|
|
204
|
+
self.tag_list = tags
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Sets prices on the master variant.
|
|
208
|
+
# Accepts array of { currency:, amount:, compare_at_amount: } hashes.
|
|
209
|
+
def prices=(prices_params)
|
|
210
|
+
find_or_build_master.prices = prices_params
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Maps 6.0 API name (category_ids) to model column (taxon_ids).
|
|
214
|
+
# Accepts both prefixed IDs and raw integer IDs.
|
|
215
|
+
def category_ids=(ids)
|
|
216
|
+
self.taxon_ids = Array(ids).filter_map do |id|
|
|
217
|
+
id.to_s.include?('_') ? Spree::Taxon.decode_prefixed_id(id) : id
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Sync media inline. Entries with `id` patch the existing asset
|
|
222
|
+
# (alt/position/variant_ids); entries with `signed_id` create + attach a
|
|
223
|
+
# fresh upload; missing items are left alone (delete still goes through
|
|
224
|
+
# the dedicated DELETE /media endpoint to avoid accidental data loss when
|
|
225
|
+
# a form ships stale state).
|
|
226
|
+
#
|
|
227
|
+
# Deferred: ActiveStorage attaches require a persisted record, so on new
|
|
228
|
+
# records we stash the params and replay them in `after_create`.
|
|
229
|
+
# @param media_params [Array<Hash>]
|
|
230
|
+
# @return [void]
|
|
231
|
+
def media=(media_params)
|
|
232
|
+
# Blank input is a no-op — never call `super` with an empty array,
|
|
233
|
+
# because the ActiveRecord collection setter would replace media with
|
|
234
|
+
# `[]` and trigger `dependent: :destroy` on every persisted asset.
|
|
235
|
+
# Explicit deletes go through the dedicated DELETE /media endpoint.
|
|
236
|
+
return if media_params.blank?
|
|
237
|
+
return super if media_params.first.is_a?(Spree::Asset)
|
|
238
|
+
|
|
239
|
+
if new_record?
|
|
240
|
+
@pending_media_params = media_params
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
apply_media(media_params)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Syncs variants from an array of hashes.
|
|
248
|
+
# Creates new variants, updates existing ones (matched by :id), and removes unlisted ones.
|
|
249
|
+
# Must be called on a persisted product (use after_save or call explicitly after create).
|
|
250
|
+
# @param variants_params [Array<Hash>] array of variant attribute hashes
|
|
251
|
+
# @return [void]
|
|
252
|
+
def variants=(variants_params)
|
|
253
|
+
return super if variants_params.blank? || variants_params.first.is_a?(Spree::Variant)
|
|
254
|
+
|
|
255
|
+
# Store for deferred processing if product is not yet persisted
|
|
256
|
+
if new_record?
|
|
257
|
+
@pending_variants_params = variants_params
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
apply_variants(variants_params)
|
|
262
|
+
end
|
|
263
|
+
|
|
200
264
|
self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at]
|
|
201
|
-
self.whitelisted_ransackable_associations = %w[taxons categories
|
|
265
|
+
self.whitelisted_ransackable_associations = %w[taxons categories store channels variants_including_master master variants tags labels
|
|
202
266
|
shipping_category classifications option_types]
|
|
203
267
|
self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon in_category in_categories price_between
|
|
204
268
|
price_lte price_gte
|
|
@@ -495,7 +559,11 @@ module Spree
|
|
|
495
559
|
@total_on_hand ||= if any_variants_not_track_inventory?
|
|
496
560
|
BigDecimal::INFINITY
|
|
497
561
|
else
|
|
498
|
-
|
|
562
|
+
if variants_including_master.loaded?
|
|
563
|
+
variants_including_master.sum(&:total_on_hand)
|
|
564
|
+
else
|
|
565
|
+
stock_items.loaded? ? stock_items.sum(&:count_on_hand) : stock_items.sum(:count_on_hand)
|
|
566
|
+
end
|
|
499
567
|
end
|
|
500
568
|
end
|
|
501
569
|
|
|
@@ -506,19 +574,6 @@ module Spree
|
|
|
506
574
|
super || variants_including_master.with_deleted.find_by(is_master: true)
|
|
507
575
|
end
|
|
508
576
|
|
|
509
|
-
# Returns the brand for the product
|
|
510
|
-
# If a brand association is defined (e.g., belongs_to :brand), it will be used
|
|
511
|
-
# Otherwise, falls back to brand_taxon for compatibility
|
|
512
|
-
# @return [Spree::Brand, Spree::Taxon]
|
|
513
|
-
def brand
|
|
514
|
-
if self.class.reflect_on_association(:brand)
|
|
515
|
-
super
|
|
516
|
-
else
|
|
517
|
-
Spree::Deprecation.warn('Spree::Product#brand is deprecated and will be removed in Spree 5.5. Please use Spree::Product#brand_taxon instead.')
|
|
518
|
-
brand_taxon
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
577
|
# Returns the brand taxon for the product
|
|
523
578
|
# @return [Spree::Taxon]
|
|
524
579
|
def brand_taxon
|
|
@@ -538,7 +593,7 @@ module Spree
|
|
|
538
593
|
# Returns the brand name for the product
|
|
539
594
|
# @return [String]
|
|
540
595
|
def brand_name
|
|
541
|
-
|
|
596
|
+
brand_taxon&.name
|
|
542
597
|
end
|
|
543
598
|
|
|
544
599
|
def main_taxon
|
|
@@ -573,15 +628,13 @@ module Spree
|
|
|
573
628
|
def auto_match_taxons
|
|
574
629
|
return if deleted?
|
|
575
630
|
return if archived?
|
|
576
|
-
|
|
577
|
-
store = stores.find_by(default: true) || stores.first
|
|
578
631
|
return if store.nil? || store.taxons.automatic.none?
|
|
579
632
|
|
|
580
633
|
Spree::Products::AutoMatchTaxonsJob.set(wait: 30.seconds).perform_later(id)
|
|
581
634
|
end
|
|
582
635
|
|
|
583
636
|
def to_csv(store = nil)
|
|
584
|
-
store ||=
|
|
637
|
+
store ||= self.store
|
|
585
638
|
properties_for_csv = if respond_to?(:product_properties) && Spree::Config.respond_to?(:product_properties_enabled) && Spree::Config[:product_properties_enabled]
|
|
586
639
|
Spree::Property.order(:position).flat_map do |property|
|
|
587
640
|
[
|
|
@@ -645,6 +698,100 @@ module Spree
|
|
|
645
698
|
nil
|
|
646
699
|
end
|
|
647
700
|
|
|
701
|
+
def pending_variants?
|
|
702
|
+
@pending_variants_params.present?
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def apply_pending_variants
|
|
706
|
+
return unless @pending_variants_params
|
|
707
|
+
|
|
708
|
+
apply_variants(@pending_variants_params)
|
|
709
|
+
@pending_variants_params = nil
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def pending_media?
|
|
713
|
+
@pending_media_params.present?
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def apply_pending_media
|
|
717
|
+
return unless @pending_media_params
|
|
718
|
+
|
|
719
|
+
apply_media(@pending_media_params)
|
|
720
|
+
@pending_media_params = nil
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def apply_media(media_params)
|
|
724
|
+
# Eager-load Asset descendants once so the type allowlist is stable across
|
|
725
|
+
# the loop (and across requests once subclasses are referenced). Computed
|
|
726
|
+
# per-call rather than at class-load to avoid forcing autoload of every
|
|
727
|
+
# Asset subclass during boot.
|
|
728
|
+
allowed_types = [Spree::Asset, *Spree::Asset.descendants].map(&:name).to_set
|
|
729
|
+
media_params.each do |raw|
|
|
730
|
+
attrs = raw.respond_to?(:to_h) ? raw.to_h : raw
|
|
731
|
+
attrs = attrs.with_indifferent_access
|
|
732
|
+
|
|
733
|
+
# Upsert path: entries with an `id` patch an existing asset (alt,
|
|
734
|
+
# position, variant_ids). Entries with a `signed_id` create+attach.
|
|
735
|
+
# Omitting an entry leaves it alone — explicit DELETE on the dedicated
|
|
736
|
+
# media endpoint is still the only way to remove an asset.
|
|
737
|
+
asset_id = attrs.delete(:id)
|
|
738
|
+
if asset_id.present?
|
|
739
|
+
asset = media.find_by_param(asset_id) || next
|
|
740
|
+
asset.update!(attrs.except(:signed_id, :type))
|
|
741
|
+
next
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
signed_id = attrs.delete(:signed_id)
|
|
745
|
+
next if signed_id.blank?
|
|
746
|
+
|
|
747
|
+
media_type = attrs.delete(:type) || 'Spree::Image'
|
|
748
|
+
next unless allowed_types.include?(media_type)
|
|
749
|
+
|
|
750
|
+
asset = media.build(attrs.except(:id))
|
|
751
|
+
asset.type = media_type
|
|
752
|
+
asset.attachment.attach(signed_id)
|
|
753
|
+
asset.save!
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def apply_variants(variants_params)
|
|
758
|
+
variant_ids_in_payload = []
|
|
759
|
+
master_touched = false
|
|
760
|
+
|
|
761
|
+
variants_params.each do |variant_data|
|
|
762
|
+
variant_data = variant_data.to_h.with_indifferent_access
|
|
763
|
+
variant_id = variant_data.delete(:id)
|
|
764
|
+
options = variant_data[:options]
|
|
765
|
+
|
|
766
|
+
if variant_id.present?
|
|
767
|
+
variant = variants_including_master.find_by_param!(variant_id)
|
|
768
|
+
variant.update!(variant_data)
|
|
769
|
+
variant_ids_in_payload << variant.id
|
|
770
|
+
elsif options.blank? || (options.is_a?(Array) && options.empty?)
|
|
771
|
+
# An entry with no options addresses the master variant. Building a
|
|
772
|
+
# non-master here would create a phantom duplicate (the auto-built
|
|
773
|
+
# master already exists, and `variants` excludes it). Upsert onto
|
|
774
|
+
# the master instead — the merchant-visible "default variant" on a
|
|
775
|
+
# simple product IS the master.
|
|
776
|
+
variant_data = variant_data.except(:options)
|
|
777
|
+
target = find_or_build_master
|
|
778
|
+
target.assign_attributes(variant_data)
|
|
779
|
+
target.save!
|
|
780
|
+
master_touched = true
|
|
781
|
+
else
|
|
782
|
+
variant = variants.build
|
|
783
|
+
variant.assign_attributes(variant_data)
|
|
784
|
+
variant.save!
|
|
785
|
+
variant_ids_in_payload << variant.id
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Remove variants not in the payload (only non-master). If only the
|
|
790
|
+
# master was touched (simple product), leave existing non-master
|
|
791
|
+
# variants alone — the payload is partial, not a full replacement.
|
|
792
|
+
variants.where.not(id: variant_ids_in_payload).destroy_all if variant_ids_in_payload.any? && !master_touched
|
|
793
|
+
end
|
|
794
|
+
|
|
648
795
|
def add_associations_from_prototype
|
|
649
796
|
if prototype_id && prototype = Spree::Prototype.find_by(id: prototype_id)
|
|
650
797
|
self.option_types = prototype.option_types
|
|
@@ -668,7 +815,7 @@ module Spree
|
|
|
668
815
|
values = option_values_hash.values
|
|
669
816
|
values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) }
|
|
670
817
|
|
|
671
|
-
default_currency =
|
|
818
|
+
default_currency = store&.default_currency || Spree::Store.default.default_currency
|
|
672
819
|
master_price = master.price_in(default_currency).amount
|
|
673
820
|
|
|
674
821
|
values.each do |ids|
|
|
@@ -678,12 +825,6 @@ module Spree
|
|
|
678
825
|
save
|
|
679
826
|
end
|
|
680
827
|
|
|
681
|
-
def default_variant_cache_key
|
|
682
|
-
Spree::Deprecation.warn('Spree::Product#default_variant_cache_key is deprecated and will be removed in Spree 5.5. Please remove any occurrences of it.')
|
|
683
|
-
|
|
684
|
-
"spree/default-variant/#{cache_key_with_version}/#{Spree::Config[:track_inventory_levels]}"
|
|
685
|
-
end
|
|
686
|
-
|
|
687
828
|
def ensure_master
|
|
688
829
|
return unless new_record?
|
|
689
830
|
|
|
@@ -691,7 +832,7 @@ module Spree
|
|
|
691
832
|
end
|
|
692
833
|
|
|
693
834
|
def assign_default_tax_category
|
|
694
|
-
self.tax_category = Spree::TaxCategory.default if new_record?
|
|
835
|
+
self.tax_category = Spree::TaxCategory.default if new_record? && self[:tax_category_id].blank?
|
|
695
836
|
end
|
|
696
837
|
|
|
697
838
|
def anything_changed?
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Per-channel publication record. A Product is "published" on a Channel when
|
|
3
|
+
# a ProductPublication exists for that pair; the optional window
|
|
4
|
+
# (+published_at+/+unpublished_at+) gates customer visibility.
|
|
5
|
+
#
|
|
6
|
+
# The owning Store is derived via +channel.store+ — no +store_id+ column
|
|
7
|
+
# lives on this table. Historic core had a +spree_products_stores+ join that
|
|
8
|
+
# also carried the Product↔Store relation; in 5.5+ that responsibility moves
|
|
9
|
+
# onto +Spree::Product#store_id+ directly, leaving this table single-purpose.
|
|
10
|
+
class ProductPublication < Spree.base_class
|
|
11
|
+
has_prefix_id :pp
|
|
12
|
+
|
|
13
|
+
belongs_to :product, class_name: 'Spree::Product', touch: true
|
|
14
|
+
belongs_to :channel, class_name: 'Spree::Channel'
|
|
15
|
+
|
|
16
|
+
validates :product, :channel, presence: true
|
|
17
|
+
validates :product_id, uniqueness: { scope: :channel_id }
|
|
18
|
+
validate :unpublished_at_after_published_at, if: -> { published_at && unpublished_at }
|
|
19
|
+
|
|
20
|
+
scope :published, lambda {
|
|
21
|
+
where('published_at IS NULL OR published_at <= ?', Time.current)
|
|
22
|
+
.where('unpublished_at IS NULL OR unpublished_at > ?', Time.current)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
self.whitelisted_ransackable_attributes = %w[product_id channel_id published_at unpublished_at]
|
|
26
|
+
self.whitelisted_ransackable_associations = %w[product channel]
|
|
27
|
+
|
|
28
|
+
delegate :store, :store_id, to: :channel
|
|
29
|
+
|
|
30
|
+
def published?
|
|
31
|
+
(published_at.nil? || published_at <= Time.current) &&
|
|
32
|
+
(unpublished_at.nil? || unpublished_at > Time.current)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def unpublished_at_after_published_at
|
|
38
|
+
return if unpublished_at > published_at
|
|
39
|
+
|
|
40
|
+
errors.add(:unpublished_at, :must_be_after_published_at)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -7,6 +7,10 @@ module Spree
|
|
|
7
7
|
|
|
8
8
|
before_validation -> { self.calculator ||= Calculator::PercentOnLineItem.new }
|
|
9
9
|
|
|
10
|
+
def self.additional_permitted_attributes
|
|
11
|
+
[calculator: [:type, { preferences: {} }]]
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def perform(options = {})
|
|
11
15
|
order = options[:order]
|
|
12
16
|
promotion = options[:promotion]
|
|
@@ -8,6 +8,17 @@ module Spree
|
|
|
8
8
|
|
|
9
9
|
after_save :handle_promotion_action_line_items
|
|
10
10
|
|
|
11
|
+
def self.additional_permitted_attributes
|
|
12
|
+
[line_items: [:variant_id, :quantity]]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# API v3 flat alias for `promotion_action_line_items_attributes`.
|
|
16
|
+
# Accepts an array of `{ variant_id:, quantity: }` rows; the list
|
|
17
|
+
# is the *desired* set, so anything missing on save is removed.
|
|
18
|
+
def line_items=(rows)
|
|
19
|
+
self.promotion_action_line_items_attributes = rows
|
|
20
|
+
end
|
|
21
|
+
|
|
11
22
|
delegate :eligible?, to: :promotion
|
|
12
23
|
|
|
13
24
|
# Adds a line item to the Order if the promotion is eligible
|
|
@@ -85,29 +96,36 @@ module Spree
|
|
|
85
96
|
|
|
86
97
|
private
|
|
87
98
|
|
|
88
|
-
# Handles the creation and
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
99
|
+
# Handles the creation, updating, and pruning of promotion action
|
|
100
|
+
# line items. The submitted list is the *desired* set — variants
|
|
101
|
+
# not present are deleted, ones that are get upserted. Accepts
|
|
102
|
+
# both the legacy Rails admin hash shape (`{ "0" => attrs }`) and
|
|
103
|
+
# a flat array from the API. Variant IDs may be raw or prefixed.
|
|
93
104
|
def handle_promotion_action_line_items
|
|
94
105
|
return unless promotion_action_line_items_attributes
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
rows = promotion_action_line_items_attributes.is_a?(Hash) ? promotion_action_line_items_attributes.values : promotion_action_line_items_attributes
|
|
108
|
+
rows = rows.map { |row| row.respond_to?(:to_h) ? row.to_h.with_indifferent_access : row.with_indifferent_access }
|
|
109
|
+
|
|
110
|
+
rows = rows.map do |row|
|
|
111
|
+
variant_id = row['variant_id']
|
|
112
|
+
variant_id = Spree::Variant.find_by_param(variant_id)&.id if Spree::PrefixedId.prefixed_id?(variant_id)
|
|
113
|
+
row.merge('variant_id' => variant_id)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
desired_variant_ids = rows.map { |row| row['variant_id'] }.compact
|
|
117
|
+
promotion_action_line_items.where.not(variant_id: desired_variant_ids).delete_all
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
records_for_upsert = promotion_action_line_items_attributes.map { |key, params| params["_destroy"] != "1" ? params : nil }.compact
|
|
119
|
+
return if rows.empty?
|
|
102
120
|
|
|
103
121
|
opts = {}
|
|
104
|
-
opts[:unique_by] = [:promotion_action_id, :variant_id] unless
|
|
122
|
+
opts[:unique_by] = [:promotion_action_id, :variant_id] unless mysql_adapter?
|
|
105
123
|
|
|
106
124
|
promotion_action_line_items.upsert_all(
|
|
107
|
-
|
|
125
|
+
rows.map do |params|
|
|
108
126
|
{
|
|
109
|
-
variant_id: params[
|
|
110
|
-
quantity: params[
|
|
127
|
+
variant_id: params['variant_id'],
|
|
128
|
+
quantity: params['quantity'],
|
|
111
129
|
promotion_action_id: id
|
|
112
130
|
}
|
|
113
131
|
end,
|