mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-25 20:46:48 +00:00
478 lines
16 KiB
Ruby
478 lines
16 KiB
Ruby
class Enterprise < ActiveRecord::Base
|
|
SELLS = %w(unspecified none own any).freeze
|
|
ENTERPRISE_SEARCH_RADIUS = 100
|
|
|
|
preference :shopfront_message, :text, default: ""
|
|
preference :shopfront_closed_message, :text, default: ""
|
|
preference :shopfront_taxon_order, :string, default: ""
|
|
preference :shopfront_order_cycle_order, :string, default: "orders_close_at"
|
|
|
|
# This is hopefully a temporary measure, pending the arrival of multiple named inventories
|
|
# for shops. We need this here to allow hubs to restrict visible variants to only those in
|
|
# their inventory if they so choose
|
|
# TODO: delegate this to a separate model instead of abusing Preferences.
|
|
preference :product_selection_from_inventory_only, :boolean, default: false
|
|
|
|
has_paper_trail only: [:owner_id, :sells], on: [:update]
|
|
|
|
self.inheritance_column = nil
|
|
|
|
acts_as_gmappable process_geocoding: false
|
|
|
|
has_many :relationships_as_parent, class_name: 'EnterpriseRelationship',
|
|
foreign_key: 'parent_id',
|
|
dependent: :destroy
|
|
has_many :relationships_as_child, class_name: 'EnterpriseRelationship',
|
|
foreign_key: 'child_id',
|
|
dependent: :destroy
|
|
has_and_belongs_to_many :groups, join_table: 'enterprise_groups_enterprises',
|
|
class_name: 'EnterpriseGroup'
|
|
has_many :producer_properties, foreign_key: 'producer_id'
|
|
has_many :properties, through: :producer_properties
|
|
has_many :supplied_products, class_name: 'Spree::Product',
|
|
foreign_key: 'supplier_id',
|
|
dependent: :destroy
|
|
has_many :distributed_orders, class_name: 'Spree::Order', foreign_key: 'distributor_id'
|
|
belongs_to :address, class_name: 'Spree::Address'
|
|
has_many :enterprise_fees
|
|
has_many :enterprise_roles, dependent: :destroy
|
|
has_many :users, through: :enterprise_roles
|
|
belongs_to :owner, class_name: 'Spree::User',
|
|
foreign_key: :owner_id,
|
|
inverse_of: :owned_enterprises
|
|
has_and_belongs_to_many :payment_methods, join_table: 'distributors_payment_methods',
|
|
class_name: 'Spree::PaymentMethod',
|
|
foreign_key: 'distributor_id'
|
|
has_many :distributor_shipping_methods, foreign_key: :distributor_id
|
|
has_many :shipping_methods, through: :distributor_shipping_methods
|
|
has_many :customers
|
|
has_many :inventory_items
|
|
has_many :tag_rules
|
|
has_one :stripe_account, dependent: :destroy
|
|
|
|
delegate :latitude, :longitude, :city, :state_name, to: :address
|
|
|
|
accepts_nested_attributes_for :address
|
|
accepts_nested_attributes_for :producer_properties, allow_destroy: true,
|
|
reject_if: lambda { |pp|
|
|
pp[:property_name].blank?
|
|
}
|
|
accepts_nested_attributes_for :tag_rules, allow_destroy: true,
|
|
reject_if: lambda { |tag_rule|
|
|
tag_rule[:preferred_customer_tags].blank?
|
|
}
|
|
|
|
has_attached_file :logo,
|
|
styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" },
|
|
url: '/images/enterprises/logos/:id/:style/:basename.:extension',
|
|
path: 'public/images/enterprises/logos/:id/:style/:basename.:extension'
|
|
|
|
has_attached_file :promo_image,
|
|
styles: {
|
|
large: ["1200x260#", :jpg],
|
|
medium: ["720x156#", :jpg],
|
|
thumb: ["100x100>", :jpg]
|
|
},
|
|
url: '/images/enterprises/promo_images/:id/:style/:basename.:extension',
|
|
path: 'public/images/enterprises/promo_images/:id/:style/:basename.:extension'
|
|
|
|
validates_attachment_content_type :logo, content_type: %r{\Aimage/.*\Z}
|
|
validates_attachment_content_type :promo_image, content_type: %r{\Aimage/.*\Z}
|
|
|
|
include Spree::Core::S3Support
|
|
supports_s3 :logo
|
|
supports_s3 :promo_image
|
|
|
|
validates :name, presence: true
|
|
validate :name_is_unique
|
|
validates :sells, presence: true, inclusion: { in: SELLS }
|
|
validates :address, presence: true, associated: true
|
|
validates :owner, presence: true
|
|
validates :permalink, uniqueness: true, presence: true
|
|
validate :shopfront_taxons
|
|
validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? }
|
|
|
|
before_validation :initialize_permalink, if: lambda { permalink.nil? }
|
|
before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? }
|
|
before_validation :set_unused_address_fields
|
|
after_validation :geocode_address
|
|
|
|
after_touch :touch_distributors
|
|
after_create :set_default_contact
|
|
after_create :relate_to_owners_enterprises
|
|
after_create :send_welcome_email
|
|
|
|
after_rollback :restore_permalink
|
|
|
|
scope :by_name, -> { order('name') }
|
|
scope :visible, -> { where(visible: true) }
|
|
scope :activated, -> { where("sells != 'unspecified'") }
|
|
scope :ready_for_checkout, lambda {
|
|
joins(:shipping_methods).
|
|
joins(:payment_methods).
|
|
merge(Spree::PaymentMethod.available).
|
|
select('DISTINCT enterprises.*')
|
|
}
|
|
scope :not_ready_for_checkout, lambda {
|
|
# When ready_for_checkout is empty, return all rows when there are no enterprises ready for
|
|
# checkout.
|
|
ready_enterprises = Enterprise.ready_for_checkout.
|
|
except(:select).
|
|
select('DISTINCT enterprises.id')
|
|
|
|
if ready_enterprises.present?
|
|
where("enterprises.id NOT IN (?)", ready_enterprises)
|
|
else
|
|
where("TRUE")
|
|
end
|
|
}
|
|
scope :is_primary_producer, -> { where(is_primary_producer: true) }
|
|
scope :is_distributor, -> { where('sells != ?', 'none') }
|
|
scope :is_hub, -> { where(sells: 'any') }
|
|
scope :supplying_variant_in, lambda { |variants|
|
|
joins(supplied_products: :variants_including_master).
|
|
where('spree_variants.id IN (?)', variants).
|
|
select('DISTINCT enterprises.*')
|
|
}
|
|
|
|
scope :with_order_cycles_as_supplier_outer, -> {
|
|
joins("
|
|
LEFT OUTER JOIN exchanges
|
|
ON (exchanges.sender_id = enterprises.id AND exchanges.incoming = 't')").
|
|
joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
|
|
}
|
|
|
|
scope :with_order_cycles_as_distributor_outer, -> {
|
|
joins("
|
|
LEFT OUTER JOIN exchanges
|
|
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')").
|
|
joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
|
|
}
|
|
|
|
scope :with_order_cycles_outer, -> {
|
|
joins("
|
|
LEFT OUTER JOIN exchanges
|
|
ON (exchanges.receiver_id = enterprises.id OR exchanges.sender_id = enterprises.id)").
|
|
joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
|
|
}
|
|
|
|
scope :with_order_cycles_and_exchange_variants_outer, -> {
|
|
with_order_cycles_as_distributor_outer.
|
|
joins("LEFT OUTER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)").
|
|
joins("LEFT OUTER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)")
|
|
}
|
|
|
|
scope :distributors_with_active_order_cycles, lambda {
|
|
with_order_cycles_as_distributor_outer.
|
|
merge(OrderCycle.active).
|
|
select('DISTINCT enterprises.*')
|
|
}
|
|
|
|
scope :distributing_products, lambda { |product_ids|
|
|
exchanges = joins("
|
|
INNER JOIN exchanges
|
|
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')
|
|
").
|
|
joins('INNER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)').
|
|
joins('INNER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)').
|
|
where('spree_variants.product_id IN (?)', product_ids).select('DISTINCT enterprises.id')
|
|
|
|
where(id: exchanges)
|
|
}
|
|
|
|
scope :managed_by, lambda { |user|
|
|
if user.has_spree_role?('admin')
|
|
scoped
|
|
else
|
|
joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id)
|
|
end
|
|
}
|
|
scope :relatives_of_one_union_others, lambda { |one, others|
|
|
where("
|
|
enterprises.id IN
|
|
(SELECT child_id FROM enterprise_relationships WHERE enterprise_relationships.parent_id=?)
|
|
OR enterprises.id IN
|
|
(SELECT parent_id FROM enterprise_relationships WHERE enterprise_relationships.child_id=?)
|
|
OR enterprises.id IN
|
|
(?)
|
|
", one, one, others)
|
|
}
|
|
|
|
# Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554
|
|
def self.distinct_count
|
|
count(distinct: true)
|
|
end
|
|
|
|
def contact
|
|
contact = users.where(enterprise_roles: { receives_notifications: true }).first
|
|
contact || owner
|
|
end
|
|
|
|
def update_contact(user_id)
|
|
enterprise_roles.update_all(["receives_notifications=(user_id=?)", user_id])
|
|
end
|
|
|
|
def activated?
|
|
contact.confirmed? && sells != 'unspecified'
|
|
end
|
|
|
|
def set_producer_property(property_name, property_value)
|
|
transaction do
|
|
property = Spree::Property.
|
|
where(name: property_name).
|
|
first_or_create!(presentation: property_name)
|
|
producer_property = ProducerProperty.
|
|
where(producer_id: id, property_id: property.id).
|
|
first_or_initialize
|
|
producer_property.value = property_value
|
|
producer_property.save!
|
|
end
|
|
end
|
|
|
|
def to_param
|
|
permalink
|
|
end
|
|
|
|
def relatives
|
|
Enterprise.where("
|
|
enterprises.id IN
|
|
(SELECT child_id FROM enterprise_relationships WHERE enterprise_relationships.parent_id=?)
|
|
OR enterprises.id IN
|
|
(SELECT parent_id FROM enterprise_relationships WHERE enterprise_relationships.child_id=?)
|
|
", id, id)
|
|
end
|
|
|
|
def plus_relatives_and_oc_producers(order_cycles)
|
|
oc_producer_ids = Exchange.in_order_cycle(order_cycles).incoming.pluck :sender_id
|
|
Enterprise.relatives_of_one_union_others(id, oc_producer_ids | [id])
|
|
end
|
|
|
|
def relatives_including_self
|
|
Enterprise.where(id: relatives.pluck(:id) | [id])
|
|
end
|
|
|
|
def distributors
|
|
relatives_including_self.is_distributor
|
|
end
|
|
|
|
def suppliers
|
|
relatives_including_self.is_primary_producer
|
|
end
|
|
|
|
def website
|
|
strip_url self[:website]
|
|
end
|
|
|
|
def facebook
|
|
strip_url self[:facebook]
|
|
end
|
|
|
|
def linkedin
|
|
strip_url self[:linkedin]
|
|
end
|
|
|
|
def inventory_variants
|
|
if prefers_product_selection_from_inventory_only?
|
|
Spree::Variant.visible_for(self)
|
|
else
|
|
Spree::Variant.not_hidden_for(self)
|
|
end
|
|
end
|
|
|
|
def distributed_variants
|
|
Spree::Variant.
|
|
joins(:product).
|
|
merge(Spree::Product.in_distributor(self)).
|
|
select('spree_variants.*')
|
|
end
|
|
|
|
def is_distributor
|
|
sells != "none"
|
|
end
|
|
|
|
def is_hub
|
|
sells == 'any'
|
|
end
|
|
|
|
# Simplify enterprise categories for frontend logic and icons, and maybe other things.
|
|
def category
|
|
# Make this crazy logic human readable so we can argue about it sanely.
|
|
cat = is_primary_producer ? "producer_" : "non_producer_"
|
|
cat << "sells_" + sells
|
|
|
|
# Map backend cases to front end cases.
|
|
case cat
|
|
when "producer_sells_any"
|
|
:producer_hub # Producer hub who sells own and others produce and supplies other hubs.
|
|
when "producer_sells_own"
|
|
:producer_shop # Producer with shopfront and supplies other hubs.
|
|
when "producer_sells_none"
|
|
:producer # Producer only supplies through others.
|
|
when "non_producer_sells_any"
|
|
:hub # Hub selling others products in order cycles.
|
|
when "non_producer_sells_own"
|
|
:hub # Wholesaler selling through own shopfront? Does this need a separate name or even exist?
|
|
when "non_producer_sells_none"
|
|
:hub_profile # Hub selling outside the system.
|
|
end
|
|
end
|
|
|
|
# Return all taxons for all distributed products
|
|
def distributed_taxons
|
|
Spree::Taxon.
|
|
joins(:products).
|
|
where('spree_products.id IN (?)', Spree::Product.in_distributor(self).select(&:id)).
|
|
select('DISTINCT spree_taxons.*')
|
|
end
|
|
|
|
def current_distributed_taxons
|
|
Spree::Taxon
|
|
.select("DISTINCT spree_taxons.*")
|
|
.joins(products: :variants_including_master)
|
|
.joins("INNER JOIN (#{current_exchange_variants.to_sql}) \
|
|
AS exchange_variants ON spree_variants.id = exchange_variants.variant_id")
|
|
end
|
|
|
|
# Return all taxons for all supplied products
|
|
def supplied_taxons
|
|
Spree::Taxon.
|
|
joins(:products).
|
|
where('spree_products.id IN (?)', Spree::Product.in_supplier(self).select(&:id)).
|
|
select('DISTINCT spree_taxons.*')
|
|
end
|
|
|
|
def ready_for_checkout?
|
|
shipping_methods.any? && payment_methods.available.any?
|
|
end
|
|
|
|
def self.find_available_permalink(test_permalink)
|
|
test_permalink = test_permalink.parameterize
|
|
test_permalink = "my-enterprise" if test_permalink.blank?
|
|
existing = Enterprise.
|
|
select(:permalink).
|
|
order(:permalink).
|
|
where("permalink LIKE ?", "#{test_permalink}%").
|
|
map(&:permalink)
|
|
|
|
if existing.include?(test_permalink)
|
|
used_indices = existing.map do |p|
|
|
p.slice!(/^#{test_permalink}/)
|
|
p.match(/^\d+$/).to_s.to_i
|
|
end.select{ |p| p }
|
|
options = (1..existing.length).to_a - used_indices
|
|
test_permalink + options.first.to_s
|
|
else
|
|
test_permalink
|
|
end
|
|
end
|
|
|
|
def can_invoice?
|
|
abn.present?
|
|
end
|
|
|
|
protected
|
|
|
|
def devise_mailer
|
|
EnterpriseMailer
|
|
end
|
|
|
|
private
|
|
|
|
def current_exchange_variants
|
|
ExchangeVariant.joins(exchange: :order_cycle)
|
|
.merge(Exchange.outgoing)
|
|
.select("DISTINCT exchange_variants.variant_id, exchanges.receiver_id AS enterprise_id")
|
|
.where("exchanges.receiver_id = ?", id)
|
|
.merge(OrderCycle.active.with_distributor(id))
|
|
end
|
|
|
|
def name_is_unique
|
|
dups = Enterprise.where(name: name)
|
|
dups = dups.where('id != ?', id) unless new_record?
|
|
|
|
errors.add :name, I18n.t(:enterprise_name_error, email: dups.first.owner.email) if dups.any?
|
|
end
|
|
|
|
def send_welcome_email
|
|
Delayed::Job.enqueue WelcomeEnterpriseJob.new(id)
|
|
end
|
|
|
|
def strip_url(url)
|
|
url.andand.sub(%r{(https?://)?}, '')
|
|
end
|
|
|
|
def set_unused_address_fields
|
|
address.firstname = address.lastname = address.phone = 'unused' if address.present?
|
|
end
|
|
|
|
def geocode_address
|
|
address.geocode if address.andand.changed?
|
|
end
|
|
|
|
def ensure_owner_is_manager
|
|
users << owner unless users.include?(owner)
|
|
end
|
|
|
|
def enforce_ownership_limit
|
|
unless owner.can_own_more_enterprises?
|
|
errors.add(:owner, I18n.t(:enterprise_owner_error, email: owner.email,
|
|
enterprise_limit: owner.enterprise_limit ))
|
|
end
|
|
end
|
|
|
|
def set_default_contact
|
|
update_contact owner_id
|
|
end
|
|
|
|
def relate_to_owners_enterprises
|
|
# When a new producer is created, it grants permissions to all pre-existing hubs
|
|
# When a new hub is created,
|
|
# - it grants permissions to all pre-existing hubs
|
|
# - all producers grant permission to it
|
|
|
|
enterprises = owner.owned_enterprises.where('enterprises.id != ?', self)
|
|
|
|
# We grant permissions to all pre-existing hubs
|
|
hub_permissions = [:add_to_order_cycle]
|
|
hub_permissions << :create_variant_overrides if is_primary_producer
|
|
enterprises.is_hub.each do |enterprise|
|
|
EnterpriseRelationship.create!(parent: self,
|
|
child: enterprise,
|
|
permissions_list: hub_permissions)
|
|
end
|
|
|
|
# All pre-existing producers grant permission to new hubs
|
|
if is_hub
|
|
enterprises.is_primary_producer.each do |enterprise|
|
|
EnterpriseRelationship.create!(parent: enterprise,
|
|
child: self,
|
|
permissions_list: [:add_to_order_cycle,
|
|
:create_variant_overrides])
|
|
end
|
|
end
|
|
end
|
|
|
|
def shopfront_taxons
|
|
unless preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/
|
|
errors.add(:shopfront_category_ordering, "must contain a list of taxons.")
|
|
end
|
|
end
|
|
|
|
def restore_permalink
|
|
# If the permalink has errors, reset it to it's original value, so we can update the form
|
|
self.permalink = permalink_was if permalink_changed? && errors[:permalink].present?
|
|
end
|
|
|
|
def initialize_permalink
|
|
self.permalink = Enterprise.find_available_permalink(name)
|
|
end
|
|
|
|
# Touch distributors without them touching their distributors.
|
|
# We avoid an infinite loop and don't need to touch the whole distributor tree.
|
|
def touch_distributors
|
|
Enterprise.distributing_products(supplied_products.select(:id)).
|
|
where('enterprises.id != ?', id).
|
|
update_all(updated_at: Time.zone.now)
|
|
end
|
|
end
|