Files
openfoodnetwork/app/models/order_cycle.rb
Cillian O'Ruanaidh fc4f951a1a Only require OrderCycleShippingMethod records if people want to override the default shipping methods
It makes things much simpler if we return all shipping methods by default without needing OrderCycleShippingMethod records to be added to the database.

Co-authored-by: Maikel <maikel@email.org.au>
2022-09-30 13:13:39 +01:00

367 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'open_food_network/scope_variant_to_hub'
class OrderCycle < ApplicationRecord
searchable_attributes :orders_open_at, :orders_close_at, :coordinator_id
searchable_scopes :active, :inactive, :active_or_complete, :upcoming, :closed, :not_closed,
:dated, :undated, :soonest_opening, :soonest_closing, :most_recently_closed
belongs_to :coordinator, class_name: 'Enterprise'
has_many :coordinator_fee_refs, class_name: 'CoordinatorFee'
has_many :coordinator_fees, through: :coordinator_fee_refs, source: :enterprise_fee,
dependent: :destroy
has_many :exchanges, dependent: :destroy
# These scope names are prepended with "cached_" because there are existing accessor methods
# :incoming_exchanges and :outgoing_exchanges.
has_many :cached_incoming_exchanges, -> { where incoming: true }, class_name: "Exchange"
has_many :cached_outgoing_exchanges, -> { where incoming: false }, class_name: "Exchange"
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { distinct }, source: :receiver, through: :cached_outgoing_exchanges
has_many :order_cycle_schedules
has_many :schedules, through: :order_cycle_schedules
has_many :order_cycle_shipping_methods
has_many :preferred_shipping_methods, class_name: "Spree::ShippingMethod",
through: :order_cycle_shipping_methods,
source: :shipping_method
has_paper_trail meta: { custom_data: proc { |order_cycle| order_cycle.schedule_ids.to_s } }
attr_accessor :incoming_exchanges, :outgoing_exchanges
before_update :reset_processed_at, if: :will_save_change_to_orders_close_at?
after_save :sync_subscriptions, if: :opening?
validates :name, :coordinator_id, presence: true
validate :at_least_one_shipping_method_selected_for_each_distributor
validate :no_invalid_order_cycle_shipping_methods
validate :orders_close_at_after_orders_open_at?
preference :product_selection_from_coordinator_inventory_only, :boolean, default: false
scope :active, lambda {
where('order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?',
Time.zone.now,
Time.zone.now)
}
scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.zone.now) }
scope :inactive, lambda {
where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?',
Time.zone.now,
Time.zone.now)
}
scope :upcoming, lambda { where('order_cycles.orders_open_at > ?', Time.zone.now) }
scope :not_closed, lambda {
where('order_cycles.orders_close_at > ? OR order_cycles.orders_close_at IS NULL', Time.zone.now)
}
scope :closed, lambda {
where('order_cycles.orders_close_at < ?',
Time.zone.now).order("order_cycles.orders_close_at DESC")
}
scope :unprocessed, -> { where(processed_at: nil) }
scope :undated, -> { where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL') }
scope :dated, -> { where('orders_open_at IS NOT NULL AND orders_close_at IS NOT NULL') }
scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') }
# This scope returns all the closed orders
scope :most_recently_closed, lambda { closed.order('order_cycles.orders_close_at DESC') }
scope :soonest_opening, lambda { upcoming.order('order_cycles.orders_open_at ASC') }
scope :by_name, -> { order('name') }
scope :with_distributor, lambda { |distributor|
joins(:exchanges).merge(Exchange.outgoing).merge(Exchange.to_enterprise(distributor))
}
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
where(coordinator_id: user.enterprises.to_a)
end
}
# Return order cycles that user coordinates, sends to or receives from
scope :visible_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
with_exchanging_enterprises_outer.
where('order_cycles.coordinator_id IN (?) OR enterprises.id IN (?)',
user.enterprises.map(&:id),
user.enterprises.map(&:id)).
select('DISTINCT order_cycles.*')
end
}
scope :with_exchanging_enterprises_outer, lambda {
joins("LEFT OUTER JOIN exchanges ON (exchanges.order_cycle_id = order_cycles.id)").
joins("LEFT OUTER JOIN enterprises
ON (enterprises.id = exchanges.sender_id OR enterprises.id = exchanges.receiver_id)")
}
scope :involving_managed_distributors_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an outgoing exchange
# ie. coordinator or distributor
joins(:exchanges).merge(Exchange.outgoing).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)',
enterprises.pluck(:id),
enterprises.pluck(:id)).
select('DISTINCT order_cycles.*')
}
scope :involving_managed_producers_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an incoming exchange
# ie. coordinator or producer
joins(:exchanges).merge(Exchange.incoming).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)',
enterprises.pluck(:id),
enterprises.pluck(:id)).
select('DISTINCT order_cycles.*')
}
def self.first_opening_for(distributor)
with_distributor(distributor).soonest_opening.first
end
def self.first_closing_for(distributor)
with_distributor(distributor).soonest_closing.first
end
def self.most_recently_closed_for(distributor)
with_distributor(distributor).most_recently_closed.first
end
# Find the earliest closing times for each distributor in an active order cycle, and return
# them in the format {distributor_id => closing_time, ...}
def self.earliest_closing_times
Hash[
Exchange.
outgoing.
joins(:order_cycle).
merge(OrderCycle.active).
group('exchanges.receiver_id').
select("exchanges.receiver_id AS receiver_id,
MIN(order_cycles.orders_close_at) AS earliest_close_at").
map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] }
]
end
def attachable_payment_methods
Spree::PaymentMethod.available(:both).
joins("INNER JOIN distributors_payment_methods
ON payment_method_id = spree_payment_methods.id").
where("distributor_id IN (?)", distributor_ids).
distinct
end
def attachable_shipping_methods
Spree::ShippingMethod.frontend.
joins(:distributor_shipping_methods).
where("distributor_id IN (?)", distributor_ids).
distinct
end
def clone!
oc = dup
oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name)
oc.orders_open_at = oc.orders_close_at = oc.mails_sent = oc.processed_at = nil
oc.coordinator_fee_ids = coordinator_fee_ids
# rubocop:disable Layout/LineLength
oc.preferred_product_selection_from_coordinator_inventory_only = preferred_product_selection_from_coordinator_inventory_only
# rubocop:enable Layout/LineLength
oc.schedule_ids = schedule_ids
oc.save!
exchanges.each { |e| e.clone!(oc) }
oc.preferred_shipping_method_ids = preferred_shipping_method_ids
sync_subscriptions
oc.reload
end
def variants
Spree::Variant.
joins(:exchanges).
merge(Exchange.in_order_cycle(self)).
select('DISTINCT spree_variants.*').
to_a # http://stackoverflow.com/q/15110166
end
def supplied_variants
exchanges.incoming.map(&:variants).flatten.uniq.reject(&:deleted?)
end
def distributed_variants
exchanges.outgoing.map(&:variants).flatten.uniq.reject(&:deleted?)
end
def variants_distributed_by(distributor)
return Spree::Variant.where("1=0") if distributor.blank?
Spree::Variant.
joins(:exchanges).
merge(distributor.inventory_variants).
merge(Exchange.in_order_cycle(self)).
merge(Exchange.outgoing).
merge(Exchange.to_enterprise(distributor))
end
def products_distributed_by(distributor)
variants_distributed_by(distributor).map(&:product).uniq
end
def products
variants.map(&:product).uniq
end
def has_distributor?(distributor)
distributors.include? distributor
end
def has_variant?(variant)
variants.include? variant
end
def dated?
!undated?
end
def undated?
orders_open_at.nil? || orders_close_at.nil?
end
def upcoming?
orders_open_at && Time.zone.now < orders_open_at
end
def open?
orders_open_at && orders_close_at &&
Time.zone.now > orders_open_at && Time.zone.now < orders_close_at
end
def closed?
orders_close_at && Time.zone.now > orders_close_at
end
def exchange_for_distributor(distributor)
exchanges.outgoing.to_enterprises([distributor]).first
end
def exchange_for_supplier(supplier)
exchanges.incoming.from_enterprises([supplier]).first
end
def receival_instructions_for(supplier)
exchange_for_supplier(supplier)&.receival_instructions
end
def pickup_time_for(distributor)
exchange_for_distributor(distributor)&.pickup_time || distributor.next_collection_at
end
def pickup_instructions_for(distributor)
exchange_for_distributor(distributor)&.pickup_instructions
end
def exchanges_carrying(variant, distributor)
exchanges.supplying_to(distributor).with_variant(variant)
end
def exchanges_supplying(order)
variant_ids_relation = Spree::LineItem.in_orders(order).select(:variant_id)
exchanges.supplying_to(order.distributor).with_any_variant(variant_ids_relation)
end
def coordinated_by?(user)
coordinator.users.include? user
end
def items_bought_by_user(user, distributor)
# The Spree::Order.complete scope only checks for completed_at date
# it does not ensure state is "complete"
orders = Spree::Order.complete.where(state: "complete",
user_id: user,
distributor_id: distributor,
order_cycle_id: self)
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
items = Spree::LineItem.includes(:variant).joins(:order).merge(orders).to_a
items.each { |li| scoper.scope(li.variant) }
end
def shipping_methods
if simple? || preferred_shipping_methods.none?
attachable_shipping_methods
else
preferred_shipping_methods
end
end
def simple?
coordinator.sells == 'own'
end
private
def all_distributors_have_at_least_one_shipping_method?
distributors.all? do |distributor|
(distributor.shipping_method_ids & preferred_shipping_method_ids).any?
end
end
def at_least_one_shipping_method_selected_for_each_distributor
return if preferred_shipping_methods.none? ||
coordinator.nil? ||
simple? ||
all_distributors_have_at_least_one_shipping_method?
errors.add(:base, :at_least_one_shipping_method_per_distributor)
end
def no_invalid_order_cycle_shipping_methods
return if order_cycle_shipping_methods.all?(&:valid?)
errors.add(
:base,
order_cycle_shipping_methods.map(&:errors).map(&:to_a).flatten.uniq.to_sentence
)
end
def opening?
(open? || upcoming?) && saved_change_to_orders_close_at? && was_closed?
end
def was_closed?
orders_close_at_previously_was.blank? || Time.zone.now > orders_close_at_previously_was
end
def sync_subscriptions
return unless schedule_ids.any?
OrderManagement::Subscriptions::ProxyOrderSyncer.new(
Subscription.where(schedule_id: schedule_ids)
).sync!
end
def orders_close_at_after_orders_open_at?
return if orders_open_at.blank? || orders_close_at.blank?
return if orders_close_at > orders_open_at
errors.add(:orders_close_at, :after_orders_open_at)
end
def reset_processed_at
return unless orders_close_at.present? && orders_close_at_was.present?
return unless orders_close_at > orders_close_at_was
self.processed_at = nil
self.mails_sent = false
end
end