Class: Spree::Products::PrepareNestedAttributes

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree/products/prepare_nested_attributes.rb

Overview

Prepares nested attributes for product updates, handling multi-store scenarios and permissions.

This service ensures that when editing a product in one store, taxon associations from other stores are preserved. This prevents accidental data loss when a store admin updates product categories in their store without affecting other stores.

Variant removal is opt-in: only variants explicitly listed in the removed_variant_ids param are marked for destruction (or collected for discontinuation when they have completed orders). Variants merely absent from variants_attributes are left untouched, so a partially rendered or broken form can never silently mass-delete variants.

Examples:

service = Spree::Products::PrepareNestedAttributes.new(
  product,
  current_store,
  params,
  current_ability
)
prepared_params = service.call

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(product, store, params, ability) ⇒ PrepareNestedAttributes

Returns a new instance of PrepareNestedAttributes.



28
29
30
31
32
33
34
# File 'app/services/spree/products/prepare_nested_attributes.rb', line 28

def initialize(product, store, params, ability)
  @product = product
  @store = store
  @params = params
  @ability = ability
  @variants_to_discontinue = []
end

Instance Attribute Details

#variants_to_discontinueObject (readonly)

Returns the value of attribute variants_to_discontinue.



26
27
28
# File 'app/services/spree/products/prepare_nested_attributes.rb', line 26

def variants_to_discontinue
  @variants_to_discontinue
end

Instance Method Details

#callObject



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/services/spree/products/prepare_nested_attributes.rb', line 36

def call
  extract_removed_variant_ids

  if params[:variants_attributes]
    params[:variants_attributes].each do |key, variant_params|
      existing_variant = variant_params[:id].presence && @product.variants.find_by(id: variant_params[:id])
      # a re-submitted variant always wins over a removal request
      variants_to_remove.delete(variant_params[:id].to_s) if variant_params[:id].present?

      variant_params.delete(:price) # remove legacy price param

      if can_update_prices?
        backfill_price_ids!(variant_params, existing_variant)

        variant_params[:prices_attributes]&.each do |price_key, price_params|
          variant_params[:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
        end
      else
        variant_params.delete(:prices_attributes)
      end

      variant_params[:option_value_variants_attributes] = update_option_value_variants(variant_params.delete(:options), existing_variant)

      variant_params.delete(:stock_items_attributes) unless can_update_stock_items?

      params[:variants_attributes].delete(key) if variant_params.blank?
    end
    params[:variants_attributes] = params[:variants_attributes].merge(removed_variants_attributes)

    params[:product_option_types_attributes] = product_option_types_params.merge(removed_product_option_types_attributes)
  elsif params[:master_attributes]
    params[:master_attributes].delete(:stock_items_attributes) unless can_update_stock_items?

    if can_update_prices?
      # If the master price is nil then mark it for destruction
      params.dig(:master_attributes, :prices_attributes)&.each do |price_key, price_params|
        params[:master_attributes][:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
      end
    else
      params[:master_attributes].delete(:prices_attributes)
    end
  end

  params.delete(:legacy_product_publications_attributes) unless can?(:manage, Spree::ProductPublication)

  # ensure the product is owned by a store
  params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?

  # The variants matrix was emptied: no variant rows re-submitted, removals sent instead.
  # Option types are detached only when the removal list covers every variant, so a
  # partial (possibly broken) submission never turns the product into a simple one.
  # Variants kept alive by discontinuation still count as removed from the matrix,
  # so the detachment can't hinge on any of them yielding a `_destroy` row.
  if params[:variants_attributes].blank? && variants_to_remove.any? && can_remove_variants?
    attributes = removed_variants_attributes

    params[:option_type_ids] = [] if removing_all_variants? && !params.key?(:option_type_ids)

    if attributes.any?
      params[:variants_attributes] = attributes
      params[:variants_attributes].permit!
    end
  end

  params
end