module Spree
  module Stock
    class Estimator
      attr_reader :order, :currency

      def initialize(order)
        @order = order
        @currency = order.currency
      end

      def shipping_rates(package, frontend_only = true)
        shipping_rates = Array.new
        shipping_methods = shipping_methods(package)
        return [] unless shipping_methods

        shipping_methods.each do |shipping_method|
          cost = calculate_cost(shipping_method, package)
          shipping_rates << shipping_method.shipping_rates.new(:cost => cost)
        end

        shipping_rates.sort_by! { |r| r.cost || 0 }

        unless shipping_rates.empty?
          if frontend_only
            shipping_rates.each do |rate|
              rate.selected = true and break if rate.shipping_method.frontend?
            end
          else
            shipping_rates.first.selected = true
          end
        end

        shipping_rates
      end

      private
      def shipping_methods(package)
        shipping_methods = package.shipping_methods
        shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package) }
        shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) }
        shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) }
        shipping_methods
      end

      def calculate_cost(shipping_method, package)
        shipping_method.calculator.compute(package)
      end
    end
  end
end

module Spree
  module Stock
    class Estimator
      attr_reader :order, :currency

      def initialize(order)
        @order = order
        @currency = order.currency
      end

      def shipping_rates(package, frontend_only = true)
        shipping_rates = Array.new
        shipping_methods = shipping_methods(package)
        return [] unless shipping_methods

        shipping_methods.each do |shipping_method|
          cost = calculate_cost(shipping_method, package)
          shipping_rates << shipping_method.shipping_rates.new(:cost => cost) unless cost.nil?
        end

        shipping_rates.sort_by! { |r| r.cost || 0 }

        unless shipping_rates.empty?
          if frontend_only
            shipping_rates.each do |rate|
              rate.selected = true and break if rate.shipping_method.frontend?
            end
          else
            shipping_rates.first.selected = true
          end
        end

        shipping_rates
      end

      private
      def shipping_methods(package)
        shipping_methods = package.shipping_methods
        shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package) }
        shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) }
        shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) }
        shipping_methods
      end

      def calculate_cost(shipping_method, package)
        shipping_method.calculator.compute(package)
      end
    end
  end
end

Allow shipping calculators to return nil values if no service is matched.
Allow shipping calculators to return nil values if no service is matched.

With shipping calculators returning 0, we run the risk of choosing and charging
a cost of $0 in cases where for example spree_active_shipping has a failed rate
query with the carrier API. This could be a very expensive mistake.

Current problem as it stands is that with spree_active_shipping returning nil, if
the calculator returns nil, no other rate options are tried and the customer sees
no shipping options in checkout.

This fixes https://github.com/spree/spree_active_shipping/issues/104

Fixes #3588

Fixes spree/spree_active_shipping#104
