2020-02-11 13:22:20 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-03-25 17:45:53 +01:00
|
|
|
# Reservation is a Slot or a Ticket booked by a member.
|
|
|
|
# Slots are for Machine, Space and Training reservations.
|
|
|
|
# Tickets are for Event reservations.
|
2020-03-25 10:16:47 +01:00
|
|
|
class Reservation < ApplicationRecord
|
2023-01-27 13:53:23 +01:00
|
|
|
include NotificationAttachedObject
|
2022-05-11 10:44:57 +02:00
|
|
|
include ICalendarConcern
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-06-04 16:50:23 +02:00
|
|
|
belongs_to :statistic_profile
|
2017-02-28 13:23:31 +01:00
|
|
|
|
|
|
|
has_many :slots_reservations, dependent: :destroy
|
|
|
|
has_many :slots, through: :slots_reservations
|
|
|
|
|
2022-07-13 16:28:43 +02:00
|
|
|
accepts_nested_attributes_for :slots_reservations, allow_destroy: true
|
2016-03-23 18:39:41 +01:00
|
|
|
belongs_to :reservable, polymorphic: true
|
|
|
|
|
2022-10-11 17:23:45 +02:00
|
|
|
has_many :tickets, dependent: :destroy
|
2016-08-25 18:41:33 +02:00
|
|
|
accepts_nested_attributes_for :tickets, allow_destroy: false
|
|
|
|
|
2021-05-25 17:28:35 +02:00
|
|
|
has_many :invoice_items, as: :object, dependent: :destroy
|
2021-05-27 16:11:23 +02:00
|
|
|
has_one :payment_schedule_object, as: :object, dependent: :destroy
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2023-03-09 13:23:42 +01:00
|
|
|
has_many :prepaid_pack_reservations, dependent: :destroy
|
|
|
|
|
2023-07-20 16:55:22 +02:00
|
|
|
belongs_to :reservation_context
|
2023-05-09 18:54:16 +02:00
|
|
|
has_many :booking_users, dependent: :destroy
|
|
|
|
accepts_nested_attributes_for :booking_users, allow_destroy: true
|
2023-07-20 16:55:22 +02:00
|
|
|
|
2022-10-11 17:23:45 +02:00
|
|
|
validates :reservable_id, :reservable_type, presence: true
|
2018-12-11 17:27:25 +01:00
|
|
|
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
|
|
|
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
2021-05-21 18:25:18 +02:00
|
|
|
validate :slots_not_locked
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2022-10-11 17:23:45 +02:00
|
|
|
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
|
2016-03-23 18:39:41 +01:00
|
|
|
after_commit :notify_member_create_reservation, on: :create
|
|
|
|
after_commit :notify_admin_member_create_reservation, on: :create
|
2021-05-21 18:25:18 +02:00
|
|
|
after_commit :extend_subscription, on: :create
|
2023-03-14 16:45:01 +01:00
|
|
|
after_commit :notify_member_limitation_reached, on: :create
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2022-10-11 17:23:45 +02:00
|
|
|
delegate :user, to: :statistic_profile
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-11-25 10:45:54 +01:00
|
|
|
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
|
|
|
def total_booked_seats(canceled: false)
|
2019-11-25 10:30:03 +01:00
|
|
|
# cases:
|
2022-07-04 16:55:02 +02:00
|
|
|
if reservable_type == 'Event'
|
|
|
|
# - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
|
|
|
|
total = nb_reserve_places
|
|
|
|
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
|
|
|
|
|
|
|
|
total = 0 unless slots_reservations.first&.canceled_at.nil?
|
|
|
|
else
|
|
|
|
# - machine/training/space reservation => 1 slot_reservation = 1 seat
|
|
|
|
total = slots_reservations.count
|
|
|
|
total -= slots_reservations.where.not(canceled_at: nil).count unless canceled
|
|
|
|
end
|
2019-01-10 16:50:54 +01:00
|
|
|
|
2016-08-30 09:47:03 +02:00
|
|
|
total
|
|
|
|
end
|
|
|
|
|
2019-11-20 17:06:42 +01:00
|
|
|
def update_event_nb_free_places
|
|
|
|
return unless reservable_type == 'Event'
|
|
|
|
|
|
|
|
reservable.update_nb_free_places
|
|
|
|
reservable.save!
|
|
|
|
end
|
|
|
|
|
2021-05-28 17:34:20 +02:00
|
|
|
def original_payment_schedule
|
|
|
|
payment_schedule_object&.payment_schedule
|
|
|
|
end
|
|
|
|
|
|
|
|
def original_invoice
|
|
|
|
invoice_items.select(:invoice_id)
|
|
|
|
.group(:invoice_id)
|
|
|
|
.map(&:invoice_id)
|
|
|
|
.map { |id| Invoice.find_by(id: id, type: nil) }
|
|
|
|
.first
|
|
|
|
end
|
|
|
|
|
2022-05-10 16:48:58 +02:00
|
|
|
# Group all slots related to this reservation by dates and by continuous time ranges
|
|
|
|
def grouped_slots
|
|
|
|
slots_by_date = slots.group_by { |slot| slot[:start_at].to_date }.transform_values { |slots| slots.sort_by { |slot| slot[:start_at] } }
|
|
|
|
result = {}
|
|
|
|
slots_by_date.each do |date, daily_slots|
|
|
|
|
result[date] = { daily_slots.first[:start_at] => [daily_slots.first] }
|
|
|
|
|
|
|
|
daily_slots[1..].each do |slot|
|
|
|
|
found = false
|
|
|
|
result[date].each do |group_start, group_slots|
|
2022-10-11 17:23:45 +02:00
|
|
|
next unless slot[:start_at] == group_slots.last[:end_at]
|
|
|
|
|
|
|
|
result[date][group_start].push(slot)
|
|
|
|
found = true
|
|
|
|
break
|
2022-05-10 16:48:58 +02:00
|
|
|
end
|
|
|
|
result[date][slot[:start_at]] = [slot] unless found
|
|
|
|
end
|
|
|
|
end
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
2016-08-10 16:33:26 +02:00
|
|
|
private
|
2019-01-10 16:50:54 +01:00
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def machine_not_already_reserved
|
2022-07-12 17:46:01 +02:00
|
|
|
slots_reservations.each do |slot|
|
|
|
|
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
2019-01-10 16:50:54 +01:00
|
|
|
reservations: { reservable_type: reservable_type, reservable_id: reservable_id },
|
2022-07-13 16:28:43 +02:00
|
|
|
slot_id: slot.slot_id,
|
2019-01-10 16:50:54 +01:00
|
|
|
canceled_at: nil
|
2022-07-12 17:46:01 +02:00
|
|
|
).count
|
|
|
|
if same_hour_slots.positive?
|
|
|
|
errors.add(:reservable, 'already reserved')
|
2016-03-23 18:39:41 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def training_not_fully_reserved
|
2022-07-12 17:46:01 +02:00
|
|
|
full = slots_reservations.map(&:slot).map(&:full?).reduce(:&)
|
|
|
|
errors.add(:reservable, 'already fully reserved') if full
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2021-05-21 18:25:18 +02:00
|
|
|
def slots_not_locked
|
|
|
|
# check that none of the reserved availabilities was locked
|
|
|
|
slots.each do |slot|
|
|
|
|
errors.add(:slots, 'locked') if slot.availability.lock
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def extend_subscription
|
2023-03-03 16:37:14 +01:00
|
|
|
::Subscriptions::ExtensionAfterReservation.new(self).extend_subscription_if_eligible
|
2021-05-21 18:25:18 +02:00
|
|
|
end
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def notify_member_create_reservation
|
2023-06-28 15:33:51 +02:00
|
|
|
if reservable_type == 'Event' && reservable.pre_registration?
|
|
|
|
NotificationCenter.call type: 'notify_member_pre_booked_reservation',
|
|
|
|
receiver: user,
|
|
|
|
attached_object: self
|
|
|
|
else
|
|
|
|
NotificationCenter.call type: 'notify_member_create_reservation',
|
|
|
|
receiver: user,
|
|
|
|
attached_object: self
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def notify_admin_member_create_reservation
|
2023-06-28 15:33:51 +02:00
|
|
|
if reservable_type == 'Event' && reservable.pre_registration?
|
|
|
|
NotificationCenter.call type: 'notify_admin_member_pre_booked_reservation',
|
|
|
|
receiver: User.admins_and_managers,
|
|
|
|
attached_object: self
|
|
|
|
else
|
|
|
|
NotificationCenter.call type: 'notify_admin_member_create_reservation',
|
|
|
|
receiver: User.admins_and_managers,
|
|
|
|
attached_object: self
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2023-03-14 16:45:01 +01:00
|
|
|
|
|
|
|
def notify_member_limitation_reached
|
|
|
|
date = ReservationLimitService.reached_limit_date(self)
|
|
|
|
return if date.nil?
|
|
|
|
|
|
|
|
NotificationCenter.call type: 'notify_member_reservation_limit_reached',
|
|
|
|
receiver: user,
|
|
|
|
attached_object: ReservationLimitService.limit(user.subscribed_plan, reservable),
|
|
|
|
meta_data: { date: date }
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|