core/app/finders/spree/products/find.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
96%
module Spree
  module Products
    class Find
      def initialize(scope:, params:, current_currency: nil)
        @scope = scope

        if current_currency.present?
          Spree::Deprecation.warn(<<-DEPRECATION, caller)
            `current_currency` param is deprecated and will be removed in Spree 5.
            Please pass `:currency` in `params` hash instead.
          DEPRECATION
        end

        @ids              = String(params.dig(:filter, :ids)).split(',')
        @skus             = String(params.dig(:filter, :skus)).split(',')
        @store            = params[:store] || Spree::Store.default
        @price            = map_prices(String(params.dig(:filter, :price)).split(','))
        @currency         = current_currency || params.dig(:filter, :currency) || params[:currency]
        @taxons           = taxon_ids(params.dig(:filter, :taxons))
        @concat_taxons    = taxon_ids(params.dig(:filter, :concat_taxons))
        @name             = params.dig(:filter, :name)
        @options          = params.dig(:filter, :options).try(:to_unsafe_hash)
        @option_value_ids = params.dig(:filter, :option_value_ids)
        @sort_by          = params.dig(:sort_by)
        @deleted          = params.dig(:filter, :show_deleted)
        @discontinued     = params.dig(:filter, :show_discontinued)
        @properties       = params.dig(:filter, :properties)
        @in_stock         = params.dig(:filter, :in_stock)
        @backorderable    = params.dig(:filter, :backorderable)
        @purchasable      = params.dig(:filter, :purchasable)
      end

      def execute
        products = by_ids(scope)
        products = by_skus(products)
        products = by_price(products)
        products = by_currency(products)
        products = by_taxons(products)
        products = by_concat_taxons(products)
        products = by_name(products)
        products = by_options(products)
        products = by_option_value_ids(products)
        products = by_properties(products)
        products = include_deleted(products)
        products = include_discontinued(products)
        products = show_only_stock(products)
        products = show_only_backorderable(products)
        products = show_only_purchasable(products)
        products = ordered(products)

        products.distinct
      end

      private

      attr_reader :ids, :skus, :price, :currency, :taxons, :concat_taxons, :name, :options, :option_value_ids, :scope,
                  :sort_by, :deleted, :discontinued, :properties, :store, :in_stock, :backorderable, :purchasable

      def ids?
        ids.present?
      end

      def skus?
        skus.present?
      end

      def price?
        price.present?
      end

      def currency?
        currency.present?
      end

      def taxons?
        taxons.present?
      end

      def concat_taxons?
        concat_taxons.present?
      end

      def name?
        name.present?
      end

      def options?
        options.present?
      end

      def option_value_ids?
        option_value_ids.present?
      end

      def sort_by?
        sort_by.present?
      end

      def properties?
        properties.present? && properties.values.reject(&:empty?).present?
      end

      def by_ids(products)
        return products unless ids?

        products.where(id: ids)
      end

      def by_skus(products)
        return products unless skus?

        products.joins(:variants_including_master).where(spree_variants: { sku: skus })
      end

      def by_price(products)
        return products unless price?

        products.price_between(price.min, price.max)
      end

      def by_currency(products)
        return products unless currency?

        products.with_currency(currency)
      end

      def by_taxons(products)
        return products unless taxons?

        products.joins(:classifications).where(Classification.table_name => { taxon_id: taxons })
      end

      def by_concat_taxons(products)
        return products unless concat_taxons?

        product_ids = Spree::Product.
                      joins(:classifications).
                      where(Classification.table_name => { taxon_id: concat_taxons }).
                      group("#{Spree::Product.table_name}.id").
                      having("COUNT(#{Spree::Product.table_name}.id) = ?", concat_taxons.length).
                      ids

        products.where(id: product_ids)
      end

      def by_name(products)
        return products unless name?

        product_name = name

        # i18n mobility scope doesn't automatically get set for query blocks (Mobility issue #599) - set it explicitly
        products.i18n { name.matches("%#{product_name}%") }
      end

      def by_options(products)
        return products unless options?

        products_ids = options.map { |key, value| products.with_option_value(key, value)&.ids }.compact.uniq
        products.where(id: products_ids.reduce(&:intersection))
      end

      def by_option_value_ids(products)
        return products unless option_value_ids?

        product_ids = Spree::Product.
                      joins(variants: :option_values).
                      where(spree_option_values: { id: option_value_ids }).
                      group("#{Spree::Product.table_name}.id, #{Spree::Variant.table_name}.id").
                      having('COUNT(spree_option_values.option_type_id) = ?', option_types_count(option_value_ids)).
                      distinct.
                      ids

        products.where(id: product_ids)
      end

      def by_properties(products)
        return products unless properties?

        product_ids = []
        index = 0

        properties.to_unsafe_hash.each do |property_filter_param, product_properties_values|
          next if property_filter_param.blank? || product_properties_values.empty?

          values = product_properties_values.split(',').reject(&:empty?).uniq.map(&:parameterize)

          next if values.empty?

          ids = scope.unscope(:order, :includes).with_property_values(property_filter_param, values).ids
          product_ids = index == 0 ? ids : product_ids & ids
          index += 1
        end

        products.where(id: product_ids)
      end

      def option_types_count(option_value_ids)
        Spree::OptionValue.
          where(id: option_value_ids).
          distinct.
          count(:option_type_id)
      end

      def ordered(products)
        return products unless sort_by?

        case sort_by
        when 'default'
          if taxons?
            products.ascend_by_taxons_min_position(taxons)
          else
            products
          end
        when 'name-a-z'
          # workaround for Mobility issue #596 - explicitly select fields to avoid error when selecting distinct
          products.i18n.
            select("#{Product.table_name}.*").select(:name).order(name: :asc)
        when 'name-z-a'
          # workaround for Mobility issue #596
          products.i18n.
            select("#{Product.table_name}.*").select(:name).order(name: :desc)
        when 'newest-first'
          products.order(available_on: :desc)
        when 'price-high-to-low'
          order_by_price(products, :descend_by_master_price)
        when 'price-low-to-high'
          order_by_price(products, :ascend_by_master_price)
        end
      end

      def include_deleted(products)
        deleted ? products.with_deleted : products.not_deleted
      end

      def include_discontinued(products)
        discontinued ? products : products.active(currency)
      end

      def show_only_stock(products)
        return products unless in_stock.to_s == 'true'

        products.in_stock
      end

      def show_only_backorderable(products)
        return products unless backorderable.to_s == 'true'

        products.backorderable
      end

      def show_only_purchasable(products)
        return products unless purchasable.to_s == 'true'

        products.in_stock_or_backorderable
      end

      def map_prices(prices)
        prices.map do |price|
          price == 'Infinity' ? BigDecimal::INFINITY : price.to_f
        end
      end

      def taxon_ids(taxons_ids)
        return if taxons_ids.nil? || taxons_ids.to_s.blank?

        taxons = store.taxons.where(id: taxons_ids.to_s.split(','))
        taxons.map(&:cached_self_and_descendants_ids).flatten.compact.uniq.map(&:to_s)
      end

      def order_by_price(scope, order_type)
        scope.
          select("#{Product.table_name}.*, #{Spree::Price.table_name}.amount").
          reorder('').
          send(order_type)
      end
    end
  end
end