2021-04-22 19:24:08 +02:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
MINUTES_PER_HOUR = 60.0
|
|
|
|
SECONDS_PER_MINUTE = 60.0
|
|
|
|
|
2021-12-20 17:08:14 +01:00
|
|
|
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
2021-04-22 19:24:08 +02:00
|
|
|
|
|
|
|
# A generic reservation added to the shopping cart
|
|
|
|
class CartItem::Reservation < CartItem::BaseItem
|
|
|
|
def initialize(customer, operator, reservable, slots)
|
|
|
|
@customer = customer
|
|
|
|
@operator = operator
|
|
|
|
@reservable = reservable
|
2022-07-13 16:28:43 +02:00
|
|
|
@slots = slots.map { |s| expand_slot(s) }
|
2021-05-31 15:39:56 +02:00
|
|
|
super
|
2021-04-22 19:24:08 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def price
|
2021-06-29 16:56:40 +02:00
|
|
|
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
2021-06-30 10:53:05 +02:00
|
|
|
prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
|
2021-04-22 19:24:08 +02:00
|
|
|
|
|
|
|
elements = { slots: [] }
|
|
|
|
amount = 0
|
|
|
|
|
|
|
|
hours_available = credits
|
2022-10-26 17:35:02 +02:00
|
|
|
grouped_slots.each_value do |slots|
|
2021-12-29 22:07:28 +01:00
|
|
|
prices = applicable_prices(slots)
|
|
|
|
slots.each_with_index do |slot, index|
|
|
|
|
amount += get_slot_price_from_prices(prices, slot, is_privileged,
|
|
|
|
elements: elements,
|
|
|
|
has_credits: (index < hours_available),
|
|
|
|
prepaid: prepaid)
|
|
|
|
end
|
2021-04-22 19:24:08 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
{ elements: elements, amount: amount }
|
|
|
|
end
|
|
|
|
|
2021-04-23 12:52:06 +02:00
|
|
|
def name
|
|
|
|
@reservable.name
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:39:56 +02:00
|
|
|
def valid?(all_items)
|
|
|
|
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) }
|
2022-12-22 17:18:34 +01:00
|
|
|
|
2022-11-29 21:02:01 +01:00
|
|
|
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
|
|
|
reservation_deadline = reservation_deadline_minutes.minutes.since
|
|
|
|
|
2021-05-31 15:39:56 +02:00
|
|
|
@slots.each do |slot|
|
2022-07-20 17:46:09 +02:00
|
|
|
slot_db = Slot.find(slot[:slot_id])
|
|
|
|
if slot_db.nil?
|
2023-01-04 11:12:02 +01:00
|
|
|
@errors[:slot] = I18n.t('cart_item_validation.slot')
|
2022-07-13 16:28:43 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
availability = Availability.find_by(id: slot[:slot_attributes][:availability_id])
|
2022-06-07 16:55:24 +02:00
|
|
|
if availability.nil?
|
2023-01-04 11:12:02 +01:00
|
|
|
@errors[:availability] = I18n.t('cart_item_validation.availability')
|
2022-06-07 16:55:24 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2022-07-20 17:46:09 +02:00
|
|
|
if slot_db.full?
|
2023-01-04 11:12:02 +01:00
|
|
|
@errors[:slot] = I18n.t('cart_item_validation.full')
|
2022-06-07 16:55:24 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2022-12-16 14:48:56 +01:00
|
|
|
if slot_db.start_at < reservation_deadline && !@operator.privileged?
|
2023-01-04 11:12:02 +01:00
|
|
|
@errors[:slot] = I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })
|
2022-11-29 21:02:01 +01:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:39:56 +02:00
|
|
|
next if availability.plan_ids.empty?
|
2022-07-20 17:46:09 +02:00
|
|
|
next if required_subscription?(availability, pending_subscription)
|
2021-05-31 15:39:56 +02:00
|
|
|
|
2023-01-04 11:12:02 +01:00
|
|
|
@errors[:availability] = I18n.t('cart_item_validation.restricted')
|
2021-05-31 15:39:56 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2021-04-22 19:24:08 +02:00
|
|
|
protected
|
|
|
|
|
|
|
|
def credits
|
|
|
|
0
|
|
|
|
end
|
|
|
|
|
2021-12-29 22:07:28 +01:00
|
|
|
##
|
|
|
|
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
|
|
|
##
|
|
|
|
def grouped_slots
|
|
|
|
return { all: @slots } unless Setting.get('extended_prices_in_same_day')
|
|
|
|
|
2022-07-13 16:28:43 +02:00
|
|
|
@slots.group_by { |slot| slot[:slot_attributes][:start_at].to_date }
|
|
|
|
end
|
|
|
|
|
|
|
|
def expand_slot(slot)
|
|
|
|
slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) })
|
2021-12-29 22:07:28 +01:00
|
|
|
end
|
|
|
|
|
2021-12-21 15:30:08 +01:00
|
|
|
##
|
|
|
|
# Compute the price of a single slot, according to the list of applicable prices.
|
|
|
|
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
|
|
|
|
# @see get_slot_price
|
|
|
|
##
|
|
|
|
def get_slot_price_from_prices(prices, slot, is_privileged, options = {})
|
|
|
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
|
|
|
|
2022-07-13 16:28:43 +02:00
|
|
|
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
2021-12-21 15:30:08 +01:00
|
|
|
price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? }
|
|
|
|
price = prices[:prices].first if price.nil?
|
2022-10-26 17:35:02 +02:00
|
|
|
hourly_rate = ((Rational(price[:price].amount.to_f) / Rational(price[:price].duration)) * Rational(MINUTES_PER_HOUR)).to_f
|
2021-12-21 15:30:08 +01:00
|
|
|
|
|
|
|
# apply the base price to the real slot duration
|
|
|
|
real_price = get_slot_price(hourly_rate, slot, is_privileged, options)
|
|
|
|
|
|
|
|
price[:duration] -= slot_minutes
|
|
|
|
|
|
|
|
real_price
|
|
|
|
end
|
|
|
|
|
2021-04-22 19:24:08 +02:00
|
|
|
##
|
|
|
|
# Compute the price of a single slot, according to the base price and the ability for an admin
|
|
|
|
# to offer the slot.
|
|
|
|
# @param hourly_rate {Number} base price of a slot
|
|
|
|
# @param slot {Hash} Slot object
|
|
|
|
# @param is_privileged {Boolean} true if the current user has a privileged role (admin or manager)
|
|
|
|
# @param [options] {Hash} optional parameters, allowing the following options:
|
|
|
|
# - elements {Array} if provided the resulting price will be append into elements.slots
|
|
|
|
# - has_credits {Boolean} true if the user still has credits for the given slot, false if not provided
|
|
|
|
# - is_division {boolean} false if the slot covers a full availability, true if it is a subdivision (default)
|
2021-06-29 16:56:40 +02:00
|
|
|
# - prepaid_minutes {Number} number of remaining prepaid minutes for the customer
|
2021-04-22 19:24:08 +02:00
|
|
|
# @return {Number} price of the slot
|
|
|
|
##
|
|
|
|
def get_slot_price(hourly_rate, slot, is_privileged, options = {})
|
|
|
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
|
|
|
|
|
|
|
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate
|
2022-07-13 16:28:43 +02:00
|
|
|
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
2021-06-30 10:53:05 +02:00
|
|
|
# apply the base price to the real slot duration
|
2021-04-22 19:24:08 +02:00
|
|
|
real_price = if options[:is_division]
|
2022-10-26 17:35:02 +02:00
|
|
|
((Rational(slot_rate) / Rational(MINUTES_PER_HOUR)) * Rational(slot_minutes)).to_f
|
2021-04-22 19:24:08 +02:00
|
|
|
else
|
|
|
|
slot_rate
|
|
|
|
end
|
2021-06-30 10:53:05 +02:00
|
|
|
# subtract free minutes from prepaid packs
|
2021-07-02 15:45:52 +02:00
|
|
|
if real_price.positive? && options[:prepaid][:minutes]&.positive?
|
2021-06-30 10:53:05 +02:00
|
|
|
consumed = slot_minutes
|
|
|
|
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
|
2022-10-26 17:35:02 +02:00
|
|
|
real_price = (Rational(slot_minutes - consumed) * (Rational(slot_rate) / Rational(MINUTES_PER_HOUR))).to_f
|
2021-06-30 10:53:05 +02:00
|
|
|
options[:prepaid][:minutes] -= consumed
|
2021-06-29 16:56:40 +02:00
|
|
|
end
|
2021-04-22 19:24:08 +02:00
|
|
|
|
|
|
|
unless options[:elements].nil?
|
|
|
|
options[:elements][:slots].push(
|
2022-07-13 16:28:43 +02:00
|
|
|
start_at: slot[:slot_attributes][:start_at],
|
2021-04-22 19:24:08 +02:00
|
|
|
price: real_price,
|
|
|
|
promo: (slot_rate != hourly_rate)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
real_price
|
|
|
|
end
|
|
|
|
|
2021-12-21 15:30:08 +01:00
|
|
|
# We determine the list of prices applicable to current reservation
|
|
|
|
# The longest available price is always used in priority.
|
2021-12-20 17:08:14 +01:00
|
|
|
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
|
2021-12-21 15:30:08 +01:00
|
|
|
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
|
|
|
# All these prices are returned to be applied to the reservation.
|
2021-12-29 22:07:28 +01:00
|
|
|
def applicable_prices(slots)
|
2022-10-26 17:35:02 +02:00
|
|
|
total_duration = slots.map do |slot|
|
|
|
|
(slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
|
|
|
end.reduce(:+)
|
2021-12-21 15:30:08 +01:00
|
|
|
rates = { prices: [] }
|
2021-12-20 17:08:14 +01:00
|
|
|
|
|
|
|
remaining_duration = total_duration
|
2021-12-21 11:14:09 +01:00
|
|
|
while remaining_duration.positive?
|
2021-12-20 17:08:14 +01:00
|
|
|
max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id))
|
|
|
|
.where(Price.arel_table[:duration].lteq(remaining_duration))
|
|
|
|
.maximum(:duration)
|
2021-12-29 22:07:28 +01:00
|
|
|
max_duration = 60 if max_duration.nil?
|
2021-12-21 15:30:08 +01:00
|
|
|
max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration)
|
2021-12-20 17:08:14 +01:00
|
|
|
|
2021-12-21 11:14:09 +01:00
|
|
|
current_duration = [remaining_duration, max_duration].min
|
2021-12-21 15:30:08 +01:00
|
|
|
rates[:prices].push(price: max_duration_price, duration: current_duration)
|
|
|
|
|
|
|
|
remaining_duration -= current_duration
|
2021-12-20 17:08:14 +01:00
|
|
|
end
|
|
|
|
|
2021-12-21 15:30:08 +01:00
|
|
|
rates[:prices].sort! { |a, b| b[:duration] <=> a[:duration] }
|
|
|
|
rates
|
2021-12-20 17:08:14 +01:00
|
|
|
end
|
|
|
|
|
2021-04-22 19:24:08 +02:00
|
|
|
##
|
|
|
|
# Compute the number of remaining hours in the users current credits (for machine or space)
|
|
|
|
##
|
2022-10-26 17:35:02 +02:00
|
|
|
def credits_hours(credits, new_plan_being_bought: false)
|
2021-04-23 17:54:59 +02:00
|
|
|
return 0 unless credits
|
|
|
|
|
2021-04-22 19:24:08 +02:00
|
|
|
hours_available = credits.hours
|
|
|
|
unless new_plan_being_bought
|
|
|
|
user_credit = @customer.users_credits.find_by(credit_id: credits.id)
|
|
|
|
hours_available = credits.hours - user_credit.hours_used if user_credit
|
|
|
|
end
|
|
|
|
hours_available
|
|
|
|
end
|
2021-05-19 18:12:52 +02:00
|
|
|
|
|
|
|
def slots_params
|
2022-07-13 16:28:43 +02:00
|
|
|
@slots.map { |slot| slot.permit(:id, :slot_id, :offered) }
|
2021-05-19 18:12:52 +02:00
|
|
|
end
|
2022-07-20 17:46:09 +02:00
|
|
|
|
|
|
|
##
|
|
|
|
# Check if the given availability requires a valid subscription. If so, check if the current customer
|
|
|
|
# has the required susbcription, otherwise, check if the operator is privileged
|
|
|
|
##
|
|
|
|
def required_subscription?(availability, pending_subscription)
|
|
|
|
(@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) ||
|
|
|
|
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
|
|
|
(@operator.manager? && @customer.id != @operator.id) ||
|
|
|
|
@operator.admin?
|
|
|
|
end
|
2021-04-22 19:24:08 +02:00
|
|
|
end
|