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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/rich_text_helper.rb +37 -0
  3. data/app/jobs/spree/imports/process_group_job.rb +46 -0
  4. data/app/models/concerns/spree/publishable.rb +29 -1
  5. data/app/models/concerns/spree/webhook_payload_redaction.rb +88 -0
  6. data/app/models/spree/address.rb +7 -0
  7. data/app/models/spree/api_key.rb +22 -0
  8. data/app/models/spree/calculator/default_tax.rb +31 -2
  9. data/app/models/spree/channel.rb +20 -2
  10. data/app/models/spree/line_item.rb +23 -0
  11. data/app/models/spree/order.rb +10 -0
  12. data/app/models/spree/shipment.rb +9 -0
  13. data/app/models/spree/tax_rate.rb +7 -0
  14. data/app/models/spree/webhook_delivery.rb +22 -0
  15. data/app/presenters/spree/data_feeds/google_presenter.rb +28 -1
  16. data/app/services/spree/addresses/update.rb +32 -25
  17. data/app/services/spree/sample_data/loader.rb +7 -0
  18. data/app/services/spree/seeds/all.rb +2 -0
  19. data/app/services/spree/seeds/api_keys.rb +8 -4
  20. data/app/services/spree/seeds/channels.rb +25 -0
  21. data/app/services/spree/seeds/customer_groups.rb +18 -0
  22. data/config/locales/en.yml +4 -0
  23. data/db/migrate/20260719000001_add_channel_id_to_spree_api_keys.rb +5 -0
  24. data/db/sample_data/channels.rb +12 -1
  25. data/db/sample_data/wholesale.rb +59 -0
  26. data/lib/generators/spree/dummy/templates/rails/database.yml +1 -0
  27. data/lib/spree/core/version.rb +1 -1
  28. data/lib/spree/events.rb +31 -7
  29. data/lib/spree/permitted_attributes.rb +1 -1
  30. data/lib/spree/testing_support/lifecycle_events.rb +12 -0
  31. metadata +10 -5
  32. 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