spree_core 5.6.0.rc2 → 5.6.0.rc4
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/rich_text_helper.rb +37 -0
- data/app/jobs/spree/imports/process_group_job.rb +46 -0
- data/app/models/concerns/spree/publishable.rb +29 -1
- data/app/models/concerns/spree/webhook_payload_redaction.rb +88 -0
- data/app/models/spree/address.rb +7 -0
- data/app/models/spree/api_key.rb +22 -0
- data/app/models/spree/calculator/default_tax.rb +31 -2
- data/app/models/spree/channel.rb +20 -2
- data/app/models/spree/line_item.rb +23 -0
- data/app/models/spree/order.rb +10 -0
- data/app/models/spree/shipment.rb +9 -0
- data/app/models/spree/tax_rate.rb +7 -0
- data/app/models/spree/webhook_delivery.rb +22 -0
- data/app/presenters/spree/data_feeds/google_presenter.rb +28 -1
- data/app/services/spree/addresses/update.rb +32 -25
- data/app/services/spree/sample_data/loader.rb +7 -0
- data/app/services/spree/seeds/all.rb +2 -0
- data/app/services/spree/seeds/api_keys.rb +8 -4
- data/app/services/spree/seeds/channels.rb +25 -0
- data/app/services/spree/seeds/customer_groups.rb +18 -0
- data/config/locales/en.yml +4 -0
- data/db/migrate/20260719000001_add_channel_id_to_spree_api_keys.rb +5 -0
- data/db/sample_data/channels.rb +12 -1
- data/db/sample_data/wholesale.rb +59 -0
- data/lib/generators/spree/dummy/templates/rails/database.yml +1 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/events.rb +31 -7
- data/lib/spree/permitted_attributes.rb +1 -1
- data/lib/spree/testing_support/lifecycle_events.rb +12 -0
- metadata +10 -5
- data/app/services/spree/products/prepare_nested_attributes.rb +0 -265
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module Products
|
|
3
|
-
# Prepares nested attributes for product updates, handling multi-store scenarios
|
|
4
|
-
# and permissions.
|
|
5
|
-
#
|
|
6
|
-
# This service ensures that when editing a product in one store, taxon associations
|
|
7
|
-
# from other stores are preserved. This prevents accidental data loss when a store
|
|
8
|
-
# admin updates product categories in their store without affecting other stores.
|
|
9
|
-
#
|
|
10
|
-
# Variant removal is opt-in: only variants explicitly listed in the
|
|
11
|
-
# `removed_variant_ids` param are marked for destruction (or collected for
|
|
12
|
-
# discontinuation when they have completed orders). Variants merely absent from
|
|
13
|
-
# `variants_attributes` are left untouched, so a partially rendered or broken
|
|
14
|
-
# form can never silently mass-delete variants.
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# service = Spree::Products::PrepareNestedAttributes.new(
|
|
18
|
-
# product,
|
|
19
|
-
# current_store,
|
|
20
|
-
# params,
|
|
21
|
-
# current_ability
|
|
22
|
-
# )
|
|
23
|
-
# prepared_params = service.call
|
|
24
|
-
#
|
|
25
|
-
class PrepareNestedAttributes
|
|
26
|
-
attr_reader :variants_to_discontinue
|
|
27
|
-
|
|
28
|
-
def initialize(product, store, params, ability)
|
|
29
|
-
@product = product
|
|
30
|
-
@store = store
|
|
31
|
-
@params = params
|
|
32
|
-
@ability = ability
|
|
33
|
-
@variants_to_discontinue = []
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def call
|
|
37
|
-
extract_removed_variant_ids
|
|
38
|
-
|
|
39
|
-
if params[:variants_attributes]
|
|
40
|
-
params[:variants_attributes].each do |key, variant_params|
|
|
41
|
-
existing_variant = variant_params[:id].presence && @product.variants.find_by(id: variant_params[:id])
|
|
42
|
-
# a re-submitted variant always wins over a removal request
|
|
43
|
-
variants_to_remove.delete(variant_params[:id].to_s) if variant_params[:id].present?
|
|
44
|
-
|
|
45
|
-
variant_params.delete(:price) # remove legacy price param
|
|
46
|
-
|
|
47
|
-
if can_update_prices?
|
|
48
|
-
backfill_price_ids!(variant_params, existing_variant)
|
|
49
|
-
|
|
50
|
-
variant_params[:prices_attributes]&.each do |price_key, price_params|
|
|
51
|
-
variant_params[:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
|
|
52
|
-
end
|
|
53
|
-
else
|
|
54
|
-
variant_params.delete(:prices_attributes)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
variant_params[:option_value_variants_attributes] = update_option_value_variants(variant_params.delete(:options), existing_variant)
|
|
58
|
-
|
|
59
|
-
variant_params.delete(:stock_items_attributes) unless can_update_stock_items?
|
|
60
|
-
|
|
61
|
-
params[:variants_attributes].delete(key) if variant_params.blank?
|
|
62
|
-
end
|
|
63
|
-
params[:variants_attributes] = params[:variants_attributes].merge(removed_variants_attributes)
|
|
64
|
-
|
|
65
|
-
params[:product_option_types_attributes] = product_option_types_params.merge(removed_product_option_types_attributes)
|
|
66
|
-
elsif params[:master_attributes]
|
|
67
|
-
params[:master_attributes].delete(:stock_items_attributes) unless can_update_stock_items?
|
|
68
|
-
|
|
69
|
-
if can_update_prices?
|
|
70
|
-
# If the master price is nil then mark it for destruction
|
|
71
|
-
params.dig(:master_attributes, :prices_attributes)&.each do |price_key, price_params|
|
|
72
|
-
params[:master_attributes][:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
|
|
73
|
-
end
|
|
74
|
-
else
|
|
75
|
-
params[:master_attributes].delete(:prices_attributes)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
params.delete(:legacy_product_publications_attributes) unless can?(:manage, Spree::ProductPublication)
|
|
80
|
-
|
|
81
|
-
# ensure the product is owned by a store
|
|
82
|
-
params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?
|
|
83
|
-
|
|
84
|
-
# The variants matrix was emptied: no variant rows re-submitted, removals sent instead.
|
|
85
|
-
# Option types are detached only when the removal list covers every variant, so a
|
|
86
|
-
# partial (possibly broken) submission never turns the product into a simple one.
|
|
87
|
-
# Variants kept alive by discontinuation still count as removed from the matrix,
|
|
88
|
-
# so the detachment can't hinge on any of them yielding a `_destroy` row.
|
|
89
|
-
if params[:variants_attributes].blank? && variants_to_remove.any? && can_remove_variants?
|
|
90
|
-
attributes = removed_variants_attributes
|
|
91
|
-
|
|
92
|
-
params[:option_type_ids] = [] if removing_all_variants? && !params.key?(:option_type_ids)
|
|
93
|
-
|
|
94
|
-
if attributes.any?
|
|
95
|
-
params[:variants_attributes] = attributes
|
|
96
|
-
params[:variants_attributes].permit!
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
params
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
attr_reader :product, :store, :params, :ability
|
|
106
|
-
|
|
107
|
-
delegate :can?, :cannot?, to: :ability
|
|
108
|
-
|
|
109
|
-
# Backfill IDs for prices_attributes entries that reference existing prices
|
|
110
|
-
# so that ActiveRecord updates them instead of inserting duplicates
|
|
111
|
-
def backfill_price_ids!(variant_params, existing_variant)
|
|
112
|
-
return unless existing_variant && variant_params[:prices_attributes]
|
|
113
|
-
|
|
114
|
-
variant_params[:prices_attributes].each do |_key, price_params|
|
|
115
|
-
next if price_params[:id].present?
|
|
116
|
-
next if price_params[:currency].blank?
|
|
117
|
-
|
|
118
|
-
existing_price = existing_variant.prices.base_prices.find_by(currency: price_params[:currency])
|
|
119
|
-
price_params[:id] = existing_price.id if existing_price
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def product_option_types_params
|
|
124
|
-
@product_option_types_params ||= {}
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def product_option_types_to_remove
|
|
128
|
-
@product_option_types_to_remove ||= product.product_option_type_ids
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Pulls `removed_variant_ids` out of the params so it never reaches Product#update.
|
|
132
|
-
# Must run before any `variants_to_remove` access.
|
|
133
|
-
def extract_removed_variant_ids
|
|
134
|
-
@removed_variant_ids = Array(params.delete(:removed_variant_ids)).map(&:to_s)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Only variants the client explicitly asked to remove, and only ones that
|
|
138
|
-
# actually belong to this product.
|
|
139
|
-
def variants_to_remove
|
|
140
|
-
@variants_to_remove ||= (@removed_variant_ids || []).uniq & all_variant_ids
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def all_variant_ids
|
|
144
|
-
@all_variant_ids ||= product.variant_ids.map(&:to_s)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def removing_all_variants?
|
|
148
|
-
(all_variant_ids - variants_to_remove).empty?
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def can_update_prices?
|
|
152
|
-
@can_update_prices ||= product.new_record? || can?(:manage, Spree::Price.new(variant_id: product.default_variant.id))
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def can_manage_option_types?
|
|
156
|
-
@can_manage_option_types ||= product.new_record? || can?(:manage_option_types, product)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def can_update_stock_items?
|
|
160
|
-
@can_update_stock_items ||= product.new_record? || can?(:manage, Spree::StockItem.new(variant_id: product.default_variant.id))
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def can_remove_variants?
|
|
164
|
-
@can_remove_variants ||= product.persisted? && can?(:destroy, product.default_variant)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def removed_variants_attributes
|
|
168
|
-
return {} unless can_remove_variants?
|
|
169
|
-
|
|
170
|
-
populate_variants_to_discontinue
|
|
171
|
-
|
|
172
|
-
attributes = {}
|
|
173
|
-
last_index = params[:variants_attributes].presence&.keys&.map(&:to_i)&.max || -1
|
|
174
|
-
variant_ids_to_destroy.each_with_index do |variant_id, index|
|
|
175
|
-
attributes[(last_index + 1 + index).to_s] = { id: variant_id, _destroy: '1' }
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
attributes
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def populate_variants_to_discontinue
|
|
182
|
-
ids = variants_to_remove.select { |vid| variant_ids_with_completed_orders.include?(vid) }
|
|
183
|
-
@variants_to_discontinue = product.variants.where(id: ids).to_a if ids.any?
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def variant_ids_to_destroy
|
|
187
|
-
variants_to_remove - variant_ids_with_completed_orders
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def variant_ids_with_completed_orders
|
|
191
|
-
@variant_ids_with_completed_orders ||=
|
|
192
|
-
product.variants
|
|
193
|
-
.joins(:orders)
|
|
194
|
-
.merge(Spree::Order.complete)
|
|
195
|
-
.reorder(nil)
|
|
196
|
-
.distinct
|
|
197
|
-
.pluck(:id)
|
|
198
|
-
.map(&:to_s)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def removed_product_option_types_attributes
|
|
202
|
-
return {} unless can_manage_option_types?
|
|
203
|
-
|
|
204
|
-
attributes = {}
|
|
205
|
-
last_index = product_option_types_params.keys.map(&:to_i).max
|
|
206
|
-
product_option_types_to_remove.each_with_index do |product_option_type_id, index|
|
|
207
|
-
attributes[(last_index + index + 1).to_s] = { id: product_option_type_id, _destroy: '1' }
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
attributes
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def update_option_value_variants(option_value_params, existing_variant)
|
|
214
|
-
return {} unless option_value_params.present?
|
|
215
|
-
return {} unless can_manage_option_types?
|
|
216
|
-
|
|
217
|
-
option_value_variant_params = {}
|
|
218
|
-
|
|
219
|
-
option_value_params.each_with_index do |opt, index|
|
|
220
|
-
option_type = Spree::OptionType.find_by_param(opt[:id]) if opt.fetch(:id)
|
|
221
|
-
option_type ||= Spree::OptionType.where(name: opt[:name].parameterize).first_or_initialize do |o|
|
|
222
|
-
o.name = o.presentation = opt[:name]
|
|
223
|
-
o.position = opt[:position]
|
|
224
|
-
o.save!
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
option_value_identificator = if opt[:option_value_name].present?
|
|
228
|
-
opt[:option_value_name]
|
|
229
|
-
else
|
|
230
|
-
opt[:option_value_presentation]
|
|
231
|
-
end.parameterize.strip
|
|
232
|
-
|
|
233
|
-
option_value = option_type.option_values.where(name: option_value_identificator).first_or_initialize do |o|
|
|
234
|
-
o.presentation = opt[:option_value_presentation]
|
|
235
|
-
o.save!
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
existing_option_value_variant = existing_variant&.option_value_variants&.find { |ovv| ovv.option_value_id == option_value.id }
|
|
239
|
-
|
|
240
|
-
option_value_variant_params[index.to_s] = { id: existing_option_value_variant&.id, option_value_id: option_value.id }.compact_blank
|
|
241
|
-
|
|
242
|
-
next if product_option_types_params.find { |_i, v| v[:option_type_id] == option_type.id }
|
|
243
|
-
|
|
244
|
-
existing_product_option_type = @product.product_option_types.find { |pot| pot.option_type_id == option_type.id }
|
|
245
|
-
|
|
246
|
-
if existing_product_option_type
|
|
247
|
-
product_option_types_to_remove.delete(existing_product_option_type.id)
|
|
248
|
-
product_option_types_params[opt[:position]] = {
|
|
249
|
-
id: existing_product_option_type.id,
|
|
250
|
-
position: opt[:position],
|
|
251
|
-
option_type_id: option_type.id
|
|
252
|
-
}
|
|
253
|
-
else
|
|
254
|
-
product_option_types_params[opt[:position]] = {
|
|
255
|
-
option_type_id: option_type.id,
|
|
256
|
-
position: opt[:position]
|
|
257
|
-
}
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
option_value_variant_params
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
end
|