# frozen_string_literal: true module Sets # Accepts a collection_hash in format: # { # 0=> {id:"7449", name:"Pommes"}, # 1=> {...} # } # class ProductSet < ModelSet attr_reader :saved_count def initialize(attributes = {}) super(Spree::Product, [], attributes) end def save @saved_count = 0 # Attempt to save all records, collecting model errors. @collection_hash.each_value.map do |product_attributes| update_product_attributes(product_attributes) end.all? end def collection_attributes=(attributes) ids = attributes.values.pluck(:id).compact # Find and load existing products in the order they are provided @collection = Spree::Product.find(ids) @collection_hash = attributes end private # A separate method of updating products was required due to an issue with # the way Rails' assign_attributes and update behave when # delegated attributes of a nested object are updated via the parent object # (ie. price of variants). Updating such attributes by themselves did not # work using: # # product.update(variants_attributes: [{ id: y, price: xx.x }]) # # and so an explicit call to update on each individual variant was # required. ie: # # variant.update( { price: xx.x } ) # def update_product_attributes(attributes) product = find_model(@collection, attributes[:id]) return if product.nil? update_product(product, attributes) end def update_product(product, attributes) return false unless update_product_only_attributes(product, attributes) update_product_variants(product, attributes) end def update_product_only_attributes(product, attributes) variant_related_attrs = [:id, :variants_attributes] product_related_attrs = attributes.except(*variant_related_attrs) return true if product_related_attrs.blank? product.assign_attributes(product_related_attrs) return true unless product.changed? validate_presence_of_unit_value_in_product(product) success = product.errors.empty? && product.save count_result(success) success end def validate_presence_of_unit_value_in_product(product) product.variants.each do |variant| validate_presence_of_unit_value_in_variant(product, variant) end end def validate_presence_of_unit_value_in_variant(product, variant) return unless %w(weight volume).include?(product.variant_unit) return if variant.unit_value.present? product.errors.add(:unit_value, "can't be blank") end def update_product_variants(product, attributes) return true unless attributes[:variants_attributes] update_variants_attributes(product, attributes[:variants_attributes]) end def update_variants_attributes(product, variants_attributes) variants_attributes.each do |attributes| create_or_update_variant(product, attributes) end product.errors.empty? end def create_or_update_variant(product, variant_attributes) variant = find_model(product.variants, variant_attributes[:id]) if variant.present? variant.assign_attributes(variant_attributes.except(:id)) variant.save if variant.changed? ExchangeVariantDeleter.new.delete(variant) if variant.saved_change_to_supplier_id? else variant = create_variant(product, variant_attributes) end # Copy any variant errors to product variant&.errors&.each do |error| # The name is namespaced to avoid confusion with product attrs of same name. product.errors.add(:"variant_#{error.attribute}", error.message) end variant&.errors.blank? end def create_variant(product, variant_attributes) return if variant_attributes.blank? # 'You need to save the variant to create a stock item before you can set stock levels.' on_hand = variant_attributes.delete(:on_hand) on_demand = variant_attributes.delete(:on_demand) variant = product.variants.create(variant_attributes) return variant if variant.errors.present? begin if on_hand || on_demand create_stock_for_variant(variant, on_demand, on_hand) else create_stock_for_variant_from_desired(variant) end rescue StandardError => e notify_bugsnag(e, product, variant, variant_attributes) raise e end variant end def count_result(saved) @saved_count += 1 if saved end def notify_bugsnag(error, product, variant, variant_attributes) Alert.raise(error) do |report| report.add_metadata( :product_set, { product: product.attributes, variant_attributes:, variant: variant.attributes } ) report.add_metadata(:product_set, :product_error, product.errors.first) if !product.valid? report.add_metadata(:product_set, :variant_error, variant.errors.first) if !variant.valid? end end def create_stock_for_variant(variant, on_demand, on_hand) variant.on_demand = on_demand if on_demand.present? variant.on_hand = on_hand.to_i if on_hand.present? end def create_stock_for_variant_from_desired(variant) variant.on_demand = variant.on_demand_desired if variant.on_demand_desired.present? variant.on_hand = variant.on_hand_desired.to_i if variant.on_hand_desired.present? end end end