SpeciesFileGroup/taxonworks

View on GitHub
app/models/loan.rb

Summary

Maintainability
A
0 mins
Test Coverage
# A Loan is the metadata that wraps/describes an exchange of specimens.
#
# @!attribute date_requested
#   @return [DateTime]
#     date request was received by lender
#
# @!attribute request_method
#   @return [String]
#     brief not as to how the request was made, not a controlled vocabulary
#
# @!attribute date_sent
#   @return [DateTime]
#     date loan was delivered to post
#
# @!attribute date_received
#   @return [DateTime]
#     date loan was recievied by recipient
#
# @!attribute date_return_expected
#   @return [DateTime]
#      date expected
#
# @!attribute recipient_address
#   @return [String]
#     address loan sent to
#
# @!attribute recipient_email
#   @return [String]
#     email address of recipient
#
# @!attribute recipient_phone
#   @return [String]
#     phone number of recipient
#
# @!attribute recipient_country
#   @return [String]
#
# @!attribute supervisor_email
#   @return [String]oe
#     email of utlimately responsible party if recient can not be
#
# @!attribute supervisor_phone
#   @return [String]
#     phone # of utlimately responsible party if recient can not be
#
# @!attribute date_closed
#   @return [DateTime]
#     date at which loan has been fully resolved and requires no additional attention
#
# @!attribute project_id
#   @return [Integer]
#   the project ID
#
# @!attribute recipient_honorific
#   @return [String]
#     as in Prof. Mrs. Dr. M. Mr. etc.
#
# TODO: Turn into a proper subclass when https://github.com/SpeciesFileGroup/taxonworks/issues/2120 implemented.
# @!attribute is_gift
#   @return [Boolean, nil]
#     when true then no return is expected
class Loan < ApplicationRecord
  include Housekeeping
  include Shared::DataAttributes
  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include SoftValidation
  include Shared::Depictions
  include Shared::Documentation
  include Shared::HasPapertrail
  include Shared::IsData

  ignore_whitespace_on(:lender_address, :recipient_address)

  CLONED_ATTRIBUTES = [
    :lender_address,
    :recipient_address,
    :recipient_email,
    :recipient_phone,
    :recipient_country,
    :supervisor_email,
    :supervisor_phone,
    :recipient_honorific,
  ].freeze

  # A Loan#id, when present values
  # from that record are copied
  # from the referenced loan, when
  # not otherwised populated
  attr_accessor :clone_from

  after_initialize :clone_attributes, if: Proc.new{|l| l.clone_from.present? && l.new_record? }

  has_many :loan_items, dependent: :restrict_with_error, inverse_of: :loan

  has_many :loan_recipient_roles, class_name: 'LoanRecipient', as: :role_object, inverse_of: :role_object
  has_many :loan_supervisor_roles, class_name: 'LoanSupervisor', as: :role_object, inverse_of: :role_object

  has_many :loan_recipients, through: :loan_recipient_roles, source: :person
  has_many :loan_supervisors, through: :loan_supervisor_roles, source: :person

  # This is not defined in HasRoles
  has_many :people, through: :roles

  not_super = lambda {supervisor_email.present?}
  validates :supervisor_email, format: {with: User::VALID_EMAIL_REGEX}, if: not_super
  validates :recipient_email, format: {with: User::VALID_EMAIL_REGEX}, if: not_super

  validates :lender_address, presence: true

  validate :requested_after_sent
  validate :requested_after_received
  validate :requested_after_expected
  validate :requested_after_closed
  validate :sent_after_received
  validate :sent_after_expected
  validate :sent_after_closed
  validate :received_after_closed
  validate :received_after_expected

  validate :gift_or_date_expected_required

  soft_validate(
    :sv_missing_documentation,
    set: :missing_documentation,
    name: 'Missing documentation',
    description: 'No documnets')

  accepts_nested_attributes_for :loan_items, allow_destroy: true, reject_if: :reject_loan_items
  accepts_nested_attributes_for :loan_supervisors, :loan_supervisor_roles, allow_destroy: true
  accepts_nested_attributes_for :loan_recipients, :loan_recipient_roles, allow_destroy: true

  scope :overdue, -> {where('now() > loans.date_return_expected AND date_closed IS NULL')}
  scope :not_overdue, -> {where('now() < loans.date_return_expected AND date_closed IS NULL')}

  # @return [Scope] of CollectionObject
  def collection_objects
    list = collection_object_ids
    if list.empty?
      CollectionObject.where('false')
    else
      CollectionObject.find(list)
    end
  end

  # @return [Boolean, nil]
  def overdue?
    if date_return_expected.present?
      Time.current.to_date > date_return_expected && date_closed.blank?
    else
      nil
    end
  end

  # @return [Integer, nil]
  def days_overdue
    if date_return_expected.present?
      (Time.current.to_date - date_return_expected).to_i
    else
      nil
    end
  end

  # @return [Integer, false]
  def days_until_due
    date_return_expected && (date_return_expected - Time.current.to_date ).to_i
  end

  # @return [Array] collection_object ids
  def collection_object_ids
    retval = []
    loan_items.each do |li|
      case li.loan_item_object_type
      when 'Container'
        retval += li.loan_item_object.all_collection_object_ids
      when 'CollectionObject'
        retval.push(li.loan_item_object_id)
      when 'Otu'
        retval += li.loan_item_object.collection_objects.pluck(:id)
      else
      end
    end
    retval
  end

  # @return [Scope]
  #   the max 10 most recently used loans
  def self.used_recently(project_id)

    a = Loan.where(project_id:, updated_at: (3.weeks.ago..1.day.from_now))
    .select(:loan_id).order(updated_at: :desc).limit(5).pluck(:id)

    t = LoanItem.arel_table
    k = Loan.arel_table

    # i is a select manager
    i = t.project(t['loan_id'], t['updated_at']).from(t)
      .where(t['updated_at'].gt( 3.weeks.ago ))
      .where(t['project_id'].eq(project_id))
      .order(t['updated_at'].desc)

    # z is a table alias
    z = i.as('recent_t')

    b = Loan.joins(
      Arel::Nodes::InnerJoin.new(z, Arel::Nodes::On.new(z['loan_id'].eq(k['id'])))
    ).pluck(:loan_id).uniq

    (a + b).uniq
  end

  def self.select_optimized(user_id, project_id)
    r = used_recently(project_id)
    h = {
        quick: [],
        pinboard: Loan.pinned_by(user_id).where(project_id:).to_a,
        recent: []
    }

    if r.empty?
      h[:quick] = Loan.pinned_by(user_id).pinboard_inserted.where(project_id:).to_a
    else
      h[:recent] = Loan.where(id: r.first(10)).to_a
      h[:quick] = (Loan.pinned_by(user_id).pinboard_inserted.where(project_id:).to_a +
          Loan.where(id: r.first(4)).to_a).uniq
    end
    h
  end

  def contains_types?
    collection_objects.each do |c|
      return true if c.type_materials.any?
    end
    false
  end

  def families
  end

  protected

  # Not used externally
  def return!
    loan_items.update_all(date_returned: Time.current)
  end

  def clone_attributes
    l = Loan.find(clone_from)
    CLONED_ATTRIBUTES.each do |a|
      write_attribute(a, l.send(a))
    end

    l.loan_recipients.each do |p|
      roles.build(type: 'LoanRecipient', person: p)
    end

    l.loan_supervisors.each do |p|
      roles.build(type: 'LoanSupervisor', person: p)
    end
  end

  def gift_or_date_expected_required
    errors.add(:date_return_expected, ' or gift status is required') if is_gift.blank? && date_return_expected.nil?
  end

  def requested_after_sent
    errors.add(:date_requested, 'must be sent after requested') if date_requested.present? && date_sent.present? && date_sent < date_requested
  end

  def requested_after_received
    errors.add(:date_requested, 'must be received after requested') if date_requested.present? && date_received.present? && date_received < date_requested
  end

  def requested_after_expected
    errors.add(:date_requested, 'must be expected after requested') if date_requested.present? && date_return_expected.present? && date_return_expected < date_requested
  end

  def requested_after_closed
    errors.add(:date_requested, 'must be closed after requested') if date_requested.present? && date_closed.present? && date_closed < date_requested
  end

  def sent_after_received
    errors.add(:date_sent, 'must be received after sent') if date_sent.present? && date_received.present? && date_received < date_sent
  end

  def sent_after_expected
    errors.add(:date_sent, 'must be expected after sent') if date_sent.present? && date_return_expected.present? && date_return_expected < date_sent
  end

  def sent_after_closed
    errors.add(:date_sent, 'must be closed after sent') if date_sent.present? && date_closed.present? && date_closed < date_sent
  end

  def received_after_closed
    errors.add(:date_received, 'must be closed after received') if date_closed.present? && date_received.present? && date_closed < date_received
  end

  def received_after_expected
    errors.add(:date_received, 'must be expected after received') if date_return_expected.present? && date_received.present? && date_return_expected < date_received
  end

  def reject_loan_items(attributed)
    attributed['global_entity'].blank? && (attributed['loan_item_object_type'].blank? && attributed['loan_item_object_id'].blank?)
  end

  def sv_missing_documentation
    soft_validations.add(:base, 'No documents') unless self.documents.any?
  end
end