armandofox/audience1st

View on GitHub
app/models/showdate.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
97%
class Showdate < ActiveRecord::Base

  include Comparable
  
  belongs_to :show
  belongs_to :seatmap
  
  delegate :patron_notes, :name, :event_type, :to => :show

  has_many :vouchers, -> { joins(:vouchertype).merge(Vouchertype.seat_vouchertypes) }
  has_many :finalized_vouchers, -> { joins(:vouchertype).merge(Vouchertype.seat_vouchertypes).merge(Voucher.finalized) }, :class_name => 'Voucher'
  has_many :all_vouchers, :class_name => 'Voucher'
  has_many :walkup_vouchers, -> { where(:walkup => true) }, :class_name => 'Voucher'
  # Workaround for story #169936179
  # has_many :customers, -> { where('customers.role >= 0').uniq(true) }, :through => :vouchers
  #   -- though should really be ':through => :finalized_vouchers'
  def customers
    # result must be an ARel Relation, otherwise we'd just write:
    # finalized_vouchers.map(&:customer).uniq.select { |c| ! c.special_customer? }
    Customer.where('role >= 0').
      includes(:vouchers).where('items.finalized' => true).where('items.showdate_id' => self.id).
      joins(:vouchertypes).where('items.vouchertype_id = vouchertypes.id').
      where('vouchertypes.category != ?', 'nonticket').
      uniq(true)
  end
  has_many :vouchertypes, -> { uniq(true) }, :through => :vouchers
  has_many :available_vouchertypes, -> { uniq(true) }, :source => :vouchertype, :through => :valid_vouchers
  has_many :valid_vouchers, :dependent => :destroy

  serialize :house_seats, Array
  
  validates :max_advance_sales, :numericality => { :greater_than_or_equal_to => 0, :only_integer => true }
  validates :house_capacity, :numericality => { :greater_than => 0, :only_integer => true }, :unless => :has_reserved_seating?
  validate :max_sales_cannot_exceed_house_cap, :unless => :stream?
  
  validates_associated :show
  validates :thedate, :presence => true, :uniqueness => {:scope => :show_id, :message => "is already a performance for this show"}
  validate :date_must_be_within_season

  validates :description, :length => {:maximum => 255}, :allow_blank => true
  validates :access_instructions, :presence => true, :if => :stream?

  validate :cannot_change_seating_type_if_existing_reservations, :on => :update
  validate :seatmap_can_accommodate_existing_reservations, :on => :update
  validate :at_most_one_stream_anytime_performance

  require_dependency 'showdate/sales_reporting'
  require_dependency 'showdate/menu_descriptions'

  # round off all showdates to the nearest minute
  before_save :truncate_showdate_to_nearest_minute

  before_update :clear_house_seats_if_seatmap_changed
  
  # virtually every dereference of a Showdate also accesses its Show,
  #  so set that up here to avoid n+1 query problems
  default_scope { includes(:show) }

  scope :general_admission, -> { where(:seatmap_id => nil) }
  scope :reserved_seating,  -> { where.not(:seatmap_id => nil) }
  scope :in_theater, -> { where(:live_stream => false).where(:stream_anytime => false) }

  def has_reserved_seating? ; !stream?  &&  !!seatmap ; end

  def open_house_seats
    house_seats - occupied_seats
  end

  def occupied_house_seats
    house_seats & occupied_seats
  end
  
  private

  def truncate_showdate_to_nearest_minute
    self.thedate.change(:sec => 0)
  end

  def clear_house_seats_if_seatmap_changed
    self.house_seats = [] if self.seatmap_id_changed?
  end

  #  validations

  def max_sales_cannot_exceed_house_cap
    if max_advance_sales.to_i > house_capacity.to_i
      errors.add(:max_advance_sales, I18n.translate('showdates.validations.cannot_exceed_house_cap')) 

    end
  end
  
  def date_must_be_within_season
    season = show.season
    from,to = Time.at_beginning_of_season(season),Time.at_end_of_season(season)
    unless thedate.between?(from,to)
      errors.add(:base, I18n.translate('showdates.validations.date_outside_season',
                                       :season => Option.humanize_season(season),
                                       :from => from.to_formatted_s(:month_day_year),                                       :to => to.to_formatted_s(:month_day_year)))
    end
  end
  
  def at_most_one_stream_anytime_performance
    return unless stream_anytime?
    showdates = show.reload.showdates
    showdates -= [self] if !new_record?
    errors.add(:base, I18n.translate('showdates.validations.already_has_stream_anytime')) if showdates.any?(&:stream_anytime?)
  end

  def cannot_change_seating_type_if_existing_reservations
    return if total_sales.empty?
    errors.add(:base, I18n.translate('showdates.validations.cannot_change_seating_type')) if (seatmap_id_was.nil? && !seatmap_id.nil?) || (!seatmap_id_was.blank? && seatmap_id.blank?)
  end
  
  def seatmap_can_accommodate_existing_reservations
    return if seatmap.blank?
    cannot_accommodate = seatmap.cannot_accommodate(self.vouchers)
    unless cannot_accommodate.empty?
      self.errors.add(:base,
        I18n.translate('showdates.validations.cannot_change_seatmap') +  '<br/>' + 
        ApplicationController.helpers.vouchers_sorted_by_seat(cannot_accommodate))
    end
  end

  public

  Showdate::Sales = Struct.new(:vouchers, :revenue_per_seat, :total_offered_for_sale)

  def self.with_reserved_seating_json(shows = Show.all)
    (shows.nil? || shows.empty?) ? Showdate.none:
    Showdate.joins(:show).
      where('seatmap_id IS NOT NULL').
      where(:show_id => shows.map(&:id)).
      map(&:id).to_json
  end

  def valid_vouchers_for_walkup
    self.valid_vouchers.
      includes(:vouchertype).
      references(:vouchertype).
      where(:vouchertypes => {:walkup_sale_allowed => true}).
      order('vouchertypes.display_order')
  end

  # builders used by controller

  def self.from_date_list(dates, sales_cutoff, params)
    # force boolean values to be false if fields are blank
    params[:stream_anytime] = false if params[:stream_anytime].blank?
    params[:live_stream] = false    if params[:live_stream].blank?
    if params[:live_stream] || params[:stream_anytime]
      params[:house_capacity] = ValidVoucher::INFINITE
      params.delete(:seatmap_id)
    elsif !params[:seatmap_id].blank?
      params[:house_capacity] = 0
    end
    params.delete(:seatmap_id) if params[:seatmap_id].to_i.zero?
    show = Show.find(params[:show_id])
    new_showdates = dates.map do |date|
      params[:thedate] = date
      show.showdates.build(params)
    end
  end
  
  # finders

  def self.current_or_next(opts={})
    buffer = opts[:grace_period] || 0
    type = opts[:type] || 'Regular Show'
    Showdate.
      includes(:show).references(:shows).
      where("showdates.thedate >= ? AND shows.event_type=?",Time.current-buffer, type).
      order("thedate").
      first  ||

      Showdate.
      includes(:show).references(:shows).
      where("shows.event_type = ?", type).
      order('thedate DESC').
      first
  end

  def self.all_showdates_for_seasons(first=Time.current.year, last=Time.current.year)
    first = Time.current.at_beginning_of_season(first)
    last = Time.current.at_end_of_season(last)
    Showdate.where('thedate BETWEEN ? and ?', first, last).order('thedate')
  end

  def inspect
    "#{self.id} #{name_and_date_with_capacity_stats}/#{max_advance_sales}"
  end
  
  def <=>(other_showdate)
    other_showdate ? thedate <=> other_showdate.thedate : 1
  end

  # pseudo-accessors

  def stream?  ;    live_stream? || stream_anytime?  ;  end
  def in_theater? ; !live_stream? && !stream_anytime?;  end
  def live?       ; !stream_anytime? ; end

  def season
    thedate.this_season
  end

  def advance_sales_open?
    valid_vouchers.any? { |vv| Time.current >= vv.start_sales }
  end
  
  def duration
    # for now a fixed amount.  in future may be settable
    150.minutes
  end

  # Calculation of available seats (for reserved seating)
  def occupied_seats
    return [] unless seatmap
    # basically, collect seat info from all vouchers for this showdate
    vouchers.map(&:seat).compact.map(&:to_s).sort
  end

  # For general admission shows, use the specified house cap; for reserved seating,
  #  it shoudl be derived from the seatmap
  def house_capacity
    has_reserved_seating? ? seatmap.seat_count : attributes['house_capacity']
  end

  def can_accommodate?(seat)
    !has_reserved_seating? ||  seat.blank?  ||  seatmap.includes_seat?(seat)
  end
end