Files
openfoodnetwork/app/models/order_cycle.rb

250 lines
8.1 KiB
Ruby

require 'open_food_network/enterprise_fee_applicator'
class OrderCycle < ActiveRecord::Base
belongs_to :coordinator, :class_name => 'Enterprise'
has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees'
has_many :exchanges, :dependent => :destroy
# TODO: DRY the incoming/outgoing clause used in several cases below
# See Spree::Product definition, scopes variants and variants_including_master
# This will require these accessors to be renamed
attr_accessor :incoming_exchanges, :outgoing_exchanges
validates_presence_of :name, :coordinator_id
scope :active, lambda { where('order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?', Time.now, Time.now) }
scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.now) }
scope :inactive, lambda { where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?', Time.now, Time.now) }
scope :upcoming, lambda { where('order_cycles.orders_open_at > ?', Time.now) }
scope :closed, lambda { where('order_cycles.orders_close_at < ?', Time.now) }
scope :undated, where(orders_open_at: nil, orders_close_at: nil)
scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') }
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 :distributing_product, lambda { |product|
joins(:exchanges).
merge(Exchange.outgoing).
merge(Exchange.with_product(product)).
select('DISTINCT order_cycles.*') }
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')
scoped
else
where('coordinator_id IN (?)', user.enterprises)
end
}
# Return order cycles that user coordinates, sends to or receives from
scope :accessible_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
else
with_exchanging_enterprises_outer.
where('order_cycles.coordinator_id IN (?) OR enterprises.id IN (?)', user.enterprises, user.enterprises).
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)')
}
def self.first_opening_for(distributor)
with_distributor(distributor).soonest_opening.first
end
def self.most_recently_closed_for(distributor)
with_distributor(distributor).most_recently_closed.first
end
def clone!
oc = self.dup
oc.name = "COPY OF #{oc.name}"
oc.orders_open_at = oc.orders_close_at = nil
oc.coordinator_fee_ids = self.coordinator_fee_ids
oc.save!
self.exchanges.each { |e| e.clone!(oc) }
oc.reload
end
def suppliers
self.exchanges.incoming.map(&:sender).uniq
end
def distributors
self.exchanges.outgoing.map(&:receiver).uniq
end
def variants
self.exchanges.map(&:variants).flatten.uniq
end
def distributed_variants
self.exchanges.outgoing.map(&:variants).flatten.uniq
end
def variants_distributed_by(distributor)
Spree::Variant.
joins(:exchanges).
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
# If a product without variants is added to an order cycle, and then some variants are added
# to that product, then the master variant is still part of the order cycle, but customers
# should not be able to purchase it.
# This method filters out such products so that the customer cannot purchase them.
def valid_products_distributed_by(distributor)
variants = variants_distributed_by(distributor)
products = variants.map(&:product).uniq
products.reject { |p| product_has_only_obsolete_master_in_distribution?(p, variants) }
end
def products
self.variants.map(&:product).uniq
end
def has_distributor?(distributor)
self.distributors.include? distributor
end
def has_variant?(variant)
self.variants.include? variant
end
def undated?
self.orders_open_at.nil? && self.orders_close_at.nil?
end
def upcoming?
self.orders_open_at && Time.now < self.orders_open_at
end
def open?
self.orders_open_at && self.orders_close_at &&
Time.now > self.orders_open_at && Time.now < self.orders_close_at
end
def closed?
self.orders_close_at && Time.now > self.orders_close_at
end
def exchange_for_distributor(distributor)
exchanges.outgoing.to_enterprises([distributor]).first
end
def pickup_time_for(distributor)
exchange_for_distributor(distributor).andand.pickup_time || distributor.next_collection_at
end
def pickup_instructions_for(distributor)
exchange_for_distributor(distributor).andand.pickup_instructions
end
# -- Fees
# TODO: The boundary of this class is ill-defined here. OrderCycle should not know about
# EnterpriseFeeApplicator. Clients should be able to query it for relevant EnterpriseFees.
# This logic would fit better in another service object.
def fees_for(variant, distributor)
per_item_enterprise_fee_applicators_for(variant, distributor).sum do |applicator|
# Spree's Calculator interface accepts Orders or LineItems,
# so we meet that interface with a struct.
# Amount is faked, this is a method on LineItem
line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price
applicator.enterprise_fee.compute_amount(line_item)
end
end
def create_line_item_adjustments_for(line_item)
variant = line_item.variant
distributor = line_item.order.distributor
per_item_enterprise_fee_applicators_for(variant, distributor).each do |applicator|
applicator.create_line_item_adjustment(line_item)
end
end
def create_order_adjustments_for(order)
per_order_enterprise_fee_applicators_for(order).each do |applicator|
applicator.create_order_adjustment(order)
end
end
private
# -- Fees
def per_item_enterprise_fee_applicators_for(variant, distributor)
fees = []
exchanges_carrying(variant, distributor).each do |exchange|
exchange.enterprise_fees.per_item.each do |enterprise_fee|
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role)
end
end
coordinator_fees.per_item.each do |enterprise_fee|
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator')
end
fees
end
def per_order_enterprise_fee_applicators_for(order)
fees = []
exchanges_supplying(order).each do |exchange|
exchange.enterprise_fees.per_order.each do |enterprise_fee|
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role)
end
end
coordinator_fees.per_order.each do |enterprise_fee|
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator')
end
fees
end
# -- Misc
# If a product without variants is added to an order cycle, and then some variants are added
# to that product, then the master variant is still part of the order cycle, but customers
# should not be able to purchase it.
# This method is used by #valid_products_distributed_by to filter out such products so that
# the customer cannot purchase them.
def product_has_only_obsolete_master_in_distribution?(product, distributed_variants)
product.has_variants? &&
distributed_variants.include?(product.master) &&
(product.variants & distributed_variants).empty?
end
def exchanges_carrying(variant, distributor)
exchanges.supplying_to(distributor).with_variant(variant)
end
def exchanges_supplying(order)
exchanges.supplying_to(order.distributor).with_any_variant(order.variants)
end
end