# frozen_string_literal: true module Spree class ShippingMethod < ApplicationRecord include CalculatedAdjustments DISPLAY_ON_OPTIONS = { both: "", back_end: "back_end" }.freeze acts_as_paranoid acts_as_taggable default_scope -> { where(deleted_at: nil) } has_many :shipping_rates, inverse_of: :shipping_method has_many :shipments, through: :shipping_rates has_many :shipping_method_categories has_many :shipping_categories, through: :shipping_method_categories has_many :distributor_shipping_methods has_many :distributors, through: :distributor_shipping_methods, class_name: 'Enterprise', foreign_key: 'distributor_id' has_and_belongs_to_many :zones, join_table: 'spree_shipping_methods_zones', class_name: 'Spree::Zone' belongs_to :tax_category, class_name: 'Spree::TaxCategory' validates :name, presence: true validate :distributor_validation validate :at_least_one_shipping_category validate :switching_to_backoffice_only_wont_leave_order_cycles_without_shipping_methods validates :display_on, inclusion: { in: DISPLAY_ON_OPTIONS.values }, allow_nil: true before_destroy :check_destroy_wont_leave_order_cycles_without_shipping_methods after_save :touch_distributors scope :managed_by, lambda { |user| if user.has_spree_role?('admin') where(nil) else joins(:distributors). where('distributors_shipping_methods.distributor_id IN (?)', user.enterprises.select(&:id)). select('DISTINCT spree_shipping_methods.*') end } scope :for_distributors, ->(distributors) { non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors }) where(id: non_unique_matches.map(&:id)) } scope :for_distributor, lambda { |distributor| joins(:distributors). where('enterprises.id = ?', distributor) } scope :by_name, -> { order('spree_shipping_methods.name ASC') } # Here we allow checkout with shipping methods without zones (see issue #3928 for details) # and also checkout with addresses outside of the zones of the selected shipping method # This method could be used, like in Spree, to validate shipping method zones on checkout. def include?(address) address.present? end def build_tracking_url(tracking) tracking_url.gsub(/:tracking/, tracking) unless tracking.blank? || tracking_url.blank? end def self.calculators spree_calculators.__send__ model_name_without_spree_namespace end # Some shipping methods are only meant to be set via backend def frontend? display_on != "back_end" end def has_distributor?(distributor) distributors.include?(distributor) end # Checks whether the shipping method is of delivery type, meaning that it # requires the user to specify a ship address at checkout. # # @return [Boolean] def delivery? require_ship_address end # Return the services (pickup, delivery) that different distributors provide, in the format: # {distributor_id => {pickup: true, delivery: false}, ...} def self.services Hash[ Spree::ShippingMethod. joins(:distributor_shipping_methods). group('distributor_id'). select("distributor_id"). select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup"). select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery"). map { |sm| [sm.distributor_id.to_i, { pickup: sm.pickup, delivery: sm.delivery }] } ] end def self.backend where("spree_shipping_methods.display_on = ?", DISPLAY_ON_OPTIONS[:back_end]) end def self.frontend where("spree_shipping_methods.display_on IS NULL OR spree_shipping_methods.display_on = ''") end private def no_active_or_upcoming_non_simple_order_cycles_with_only_one_shipping_method? return true if new_record? OrderCycle.active.or(OrderCycle.upcoming).joins(:coordinator, :shipping_methods).where(" sells != 'own' AND spree_shipping_methods.id = ? AND NOT EXISTS( SELECT 1 FROM order_cycle_shipping_methods WHERE order_cycle_id = order_cycles.id AND shipping_method_id != ? )", id, id).none? end def at_least_one_shipping_category return unless shipping_categories.empty? errors.add(:base, "You need to select at least one shipping category") end def touch_distributors distributors.each do |distributor| distributor.touch if distributor.persisted? end end def distributor_validation validates_with DistributorsValidator end def check_destroy_wont_leave_order_cycles_without_shipping_methods return if no_active_or_upcoming_non_simple_order_cycles_with_only_one_shipping_method? errors.add(:base, :destroy_leaves_order_cycles_without_shipping_methods) throw :abort end def switching_to_backoffice_only_wont_leave_order_cycles_without_shipping_methods return if frontend? || no_active_or_upcoming_non_simple_order_cycles_with_only_one_shipping_method? errors.add(:base, :switching_to_backoffice_only_leaves_order_cycles_without_shipping_methods) end end end