<commit_before># frozen_string_literal: true

require 'open_food_network/enterprise_fee_calculator'
require 'variant_units/variant_and_line_item_naming'
require 'concerns/variant_stock'
require 'spree/localized_number'

module Spree
  class Variant < ActiveRecord::Base
    extend Spree::LocalizedNumber
    include VariantUnits::VariantAndLineItemNaming
    include VariantStock

    acts_as_paranoid

    belongs_to :product, touch: true, class_name: 'Spree::Product'
    delegate_belongs_to :product, :name, :description, :permalink, :available_on,
                        :tax_category_id, :shipping_category_id, :meta_description,
                        :meta_keywords, :tax_category, :shipping_category

    has_many :inventory_units, inverse_of: :variant
    has_many :line_items, inverse_of: :variant

    has_many :stock_items, dependent: :destroy, inverse_of: :variant
    has_many :stock_locations, through: :stock_items
    has_many :stock_movements

    has_and_belongs_to_many :option_values, join_table: :spree_option_values_variants

    has_many :images, -> { order(:position) }, as: :viewable,
                                               dependent: :destroy,
                                               class_name: "Spree::Image"
    accepts_nested_attributes_for :images

    has_one :default_price,
            -> { where currency: Spree::Config[:currency] },
            class_name: 'Spree::Price',
            dependent: :destroy
    has_many :prices,
             class_name: 'Spree::Price',
             dependent: :destroy
    delegate_belongs_to :default_price, :display_price, :display_amount,
                        :price, :price=, :currency

    has_many :exchange_variants
    has_many :exchanges, through: :exchange_variants
    has_many :variant_overrides
    has_many :inventory_items

    localize_number :price, :weight

    validate :check_price
    validates :price, numericality: { greater_than_or_equal_to: 0 },
                      presence: true,
                      if: proc { Spree::Config[:require_master_price] }

    validates :unit_value, presence: true, if: ->(variant) {
      %w(weight volume).include?(variant.product.andand.variant_unit)
    }

    validates :unit_description, presence: true, if: ->(variant) {
      variant.product.andand.variant_unit.present? && variant.unit_value.nil?
    }

    before_validation :set_cost_currency
    before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
    before_validation :ensure_unit_value

    after_save :save_default_price
    after_save :update_units

    after_create :create_stock_items
    after_create :set_position

    around_destroy :destruction

    # default variant scope only lists non-deleted variants
    scope :deleted, lambda { where('deleted_at IS NOT NULL') }

    scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }

    scope :not_master, -> { where(is_master: false) }
    scope :in_order_cycle, lambda { |order_cycle|
      with_order_cycles_inner.
        merge(Exchange.outgoing).
        where('order_cycles.id = ?', order_cycle).
        select('DISTINCT spree_variants.*')
    }

    scope :in_schedule, lambda { |schedule|
      joins(exchanges: { order_cycle: :schedules }).
        merge(Exchange.outgoing).
        where(schedules: { id: schedule }).
        select('DISTINCT spree_variants.*')
    }

    scope :for_distribution, lambda { |order_cycle, distributor|
      where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor).
        select(&:id))
    }

    scope :visible_for, lambda { |enterprise|
      joins(:inventory_items).
        where(
          'inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)',
          enterprise,
          true
        )
    }

    scope :not_hidden_for, lambda { |enterprise|
      return where("1=0") if enterprise.blank?

      joins("
        LEFT OUTER JOIN (SELECT *
                           FROM inventory_items
                           WHERE enterprise_id = #{sanitize enterprise.andand.id})
          AS o_inventory_items
          ON o_inventory_items.variant_id = spree_variants.id")
        .where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
    }

    scope :stockable_by, lambda { |enterprise|
      return where("1=0") if enterprise.blank?

      joins(:product).
        where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
    }

    # Define sope as class method to allow chaining with other scopes filtering id.
    # In Rails 3, merging two scopes on the same column will consider only the last scope.
    def self.in_distributor(distributor)
      where(id: ExchangeVariant.select(:variant_id).
                joins(:exchange).
                where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
    end

    def self.indexed
      where(nil).index_by(&:id)
    end

    def self.active(currency = nil)
      # "where(id:" is necessary so that the returned relation has no includes
      # The relation without includes will not be readonly and allow updates on it
      where("spree_variants.id in (?)", joins(:prices).
                                          where(deleted_at: nil).
                                          where('spree_prices.currency' =>
                                            currency || Spree::Config[:currency]).
                                          where('spree_prices.amount IS NOT NULL').
                                          select("spree_variants.id"))
    end

    # Allow variant to access associated soft-deleted prices.
    def default_price
      Spree::Price.unscoped { super }
    end

    def price_with_fees(distributor, order_cycle)
      price + fees_for(distributor, order_cycle)
    end

    def fees_for(distributor, order_cycle)
      OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self
    end

    def fees_by_type_for(distributor, order_cycle)
      OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self
    end

    # returns number of units currently on backorder for this variant.
    def on_backorder
      inventory_units.with_state('backordered').size
    end

    # use deleted? rather than checking the attribute directly. this
    # allows extensions to override deleted? if they want to provide
    # their own definition.
    def deleted?
      deleted_at<commit_msg>Delete dead code Variant#on_backorder<commit_after># frozen_string_literal: true

require 'open_food_network/enterprise_fee_calculator'
require 'variant_units/variant_and_line_item_naming'
require 'concerns/variant_stock'
require 'spree/localized_number'

module Spree
  class Variant < ActiveRecord::Base
    extend Spree::LocalizedNumber
    include VariantUnits::VariantAndLineItemNaming
    include VariantStock

    acts_as_paranoid

    belongs_to :product, touch: true, class_name: 'Spree::Product'
    delegate_belongs_to :product, :name, :description, :permalink, :available_on,
                        :tax_category_id, :shipping_category_id, :meta_description,
                        :meta_keywords, :tax_category, :shipping_category

    has_many :inventory_units, inverse_of: :variant
    has_many :line_items, inverse_of: :variant

    has_many :stock_items, dependent: :destroy, inverse_of: :variant
    has_many :stock_locations, through: :stock_items
    has_many :stock_movements

    has_and_belongs_to_many :option_values, join_table: :spree_option_values_variants

    has_many :images, -> { order(:position) }, as: :viewable,
                                               dependent: :destroy,
                                               class_name: "Spree::Image"
    accepts_nested_attributes_for :images

    has_one :default_price,
            -> { where currency: Spree::Config[:currency] },
            class_name: 'Spree::Price',
            dependent: :destroy
    has_many :prices,
             class_name: 'Spree::Price',
             dependent: :destroy
    delegate_belongs_to :default_price, :display_price, :display_amount,
                        :price, :price=, :currency

    has_many :exchange_variants
    has_many :exchanges, through: :exchange_variants
    has_many :variant_overrides
    has_many :inventory_items

    localize_number :price, :weight

    validate :check_price
    validates :price, numericality: { greater_than_or_equal_to: 0 },
                      presence: true,
                      if: proc { Spree::Config[:require_master_price] }

    validates :unit_value, presence: true, if: ->(variant) {
      %w(weight volume).include?(variant.product.andand.variant_unit)
    }

    validates :unit_description, presence: true, if: ->(variant) {
      variant.product.andand.variant_unit.present? && variant.unit_value.nil?
    }

    before_validation :set_cost_currency
    before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
    before_validation :ensure_unit_value

    after_save :save_default_price
    after_save :update_units

    after_create :create_stock_items
    after_create :set_position

    around_destroy :destruction

    # default variant scope only lists non-deleted variants
    scope :deleted, lambda { where('deleted_at IS NOT NULL') }

    scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }

    scope :not_master, -> { where(is_master: false) }
    scope :in_order_cycle, lambda { |order_cycle|
      with_order_cycles_inner.
        merge(Exchange.outgoing).
        where('order_cycles.id = ?', order_cycle).
        select('DISTINCT spree_variants.*')
    }

    scope :in_schedule, lambda { |schedule|
      joins(exchanges: { order_cycle: :schedules }).
        merge(Exchange.outgoing).
        where(schedules: { id: schedule }).
        select('DISTINCT spree_variants.*')
    }

    scope :for_distribution, lambda { |order_cycle, distributor|
      where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor).
        select(&:id))
    }

    scope :visible_for, lambda { |enterprise|
      joins(:inventory_items).
        where(
          'inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)',
          enterprise,
          true
        )
    }

    scope :not_hidden_for, lambda { |enterprise|
      return where("1=0") if enterprise.blank?

      joins("
        LEFT OUTER JOIN (SELECT *
                           FROM inventory_items
                           WHERE enterprise_id = #{sanitize enterprise.andand.id})
          AS o_inventory_items
          ON o_inventory_items.variant_id = spree_variants.id")
        .where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
    }

    scope :stockable_by, lambda { |enterprise|
      return where("1=0") if enterprise.blank?

      joins(:product).
        where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
    }

    # Define sope as class method to allow chaining with other scopes filtering id.
    # In Rails 3, merging two scopes on the same column will consider only the last scope.
    def self.in_distributor(distributor)
      where(id: ExchangeVariant.select(:variant_id).
                joins(:exchange).
                where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
    end

    def self.indexed
      where(nil).index_by(&:id)
    end

    def self.active(currency = nil)
      # "where(id:" is necessary so that the returned relation has no includes
      # The relation without includes will not be readonly and allow updates on it
      where("spree_variants.id in (?)", joins(:prices).
                                          where(deleted_at: nil).
                                          where('spree_prices.currency' =>
                                            currency || Spree::Config[:currency]).
                                          where('spree_prices.amount IS NOT NULL').
                                          select("spree_variants.id"))
    end

    # Allow variant to access associated soft-deleted prices.
    def default_price
      Spree::Price.unscoped { super }
    end

    def price_with_fees(distributor, order_cycle)
      price + fees_for(distributor, order_cycle)
    end

    def fees_for(distributor, order_cycle)
      OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self
    end

    def fees_by_type_for(distributor, order_cycle)
      OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self
    end

    # use deleted? rather than checking the attribute directly. this
    # allows extensions to override deleted? if they want to provide
    # their own definition.
    def deleted?
      deleted_at
{'fraction_non_alphanumeric': 0.09023898199875853, 'fraction_numerical': 0.000931098696461825, 'mean_word_length': 2.6794176420211246, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 28, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4596316', 'n_tokens_mistral': 3774, 'n_tokens_neox': 3635, 'n_words': 832}