2019-09-10 11:46:14 +02:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-05-11 13:52:27 +02:00
|
|
|
MINUTES_PER_HOUR = 60.0
|
|
|
|
SECONDS_PER_MINUTE = 60.0
|
2020-05-05 17:56:47 +02:00
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
# Store customized price for various items (Machine, Space), depending on the group and on the plan
|
|
|
|
# Also provides a static helper method to compute the price details of a shopping cart
|
2020-03-25 10:16:47 +01:00
|
|
|
class Price < ApplicationRecord
|
2016-03-23 18:39:41 +01:00
|
|
|
belongs_to :group
|
|
|
|
belongs_to :plan
|
|
|
|
belongs_to :priceable, polymorphic: true
|
|
|
|
|
|
|
|
validates :priceable, :group_id, :amount, presence: true
|
2019-09-10 11:46:14 +02:00
|
|
|
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id] }
|
|
|
|
|
|
|
|
class << self
|
|
|
|
|
|
|
|
##
|
|
|
|
# @param admin {Boolean} true if the current user (ie.the user who requests the price) is an admin
|
|
|
|
# @param user {User} The user who's reserving (or selected if an admin is reserving)
|
|
|
|
# @param reservable {Machine|Training|Event} what the reservation is targeting
|
|
|
|
# @param slots {Array<Slot>} when did the reservation will occur
|
2020-11-03 16:50:11 +01:00
|
|
|
# @param options {plan_id:Number, nb_places:Number, tickets:Array<Ticket>, coupon_code:String, payment_schedule:Boolean}
|
|
|
|
# - plan_id {Number} if the user is subscribing to a plan at the same time of his reservation, specify the plan's ID here
|
|
|
|
# - nb_places {Number} for _reservable_ of type Event, pass here the number of booked places
|
|
|
|
# - tickets {Array<Ticket>} for _reservable_ of type Event, mapping of the number of seats booked per price's category
|
|
|
|
# - coupon_code {String} Code of the coupon to apply to the total price
|
|
|
|
# - payment_schedule {Boolean} if the user is requesting a payment schedule for his subscription
|
2019-09-10 11:46:14 +02:00
|
|
|
# @return {Hash} total and price detail
|
|
|
|
##
|
2020-11-03 16:50:11 +01:00
|
|
|
def compute(admin, user, reservable, slots, options = {})
|
2019-09-10 11:46:14 +02:00
|
|
|
total_amount = 0
|
|
|
|
all_elements = {}
|
|
|
|
all_elements[:slots] = []
|
|
|
|
|
|
|
|
# initialize Plan
|
2020-03-25 17:45:53 +01:00
|
|
|
plan = if user.subscribed_plan
|
|
|
|
new_plan_being_bought = false
|
|
|
|
user.subscribed_plan
|
2020-11-03 16:50:11 +01:00
|
|
|
elsif options[:plan_id]
|
2020-03-25 17:45:53 +01:00
|
|
|
new_plan_being_bought = true
|
2020-11-03 16:50:11 +01:00
|
|
|
Plan.find(options[:plan_id])
|
2020-03-25 17:45:53 +01:00
|
|
|
else
|
|
|
|
new_plan_being_bought = false
|
|
|
|
nil
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
# === compute reservation price ===
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
case reservable
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
# Machine reservation
|
|
|
|
when Machine
|
|
|
|
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
|
|
if plan
|
2020-05-11 13:52:27 +02:00
|
|
|
machine_credit = plan.machine_credits.select { |credit| credit.creditable_id == reservable.id }.first
|
|
|
|
if machine_credit
|
|
|
|
hours_available = credits_hours(machine_credit, user, new_plan_being_bought)
|
2016-03-23 18:39:41 +01:00
|
|
|
slots.each_with_index do |slot, index|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements, has_credits: (index < hours_available))
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
else
|
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Training reservation
|
|
|
|
when Training
|
|
|
|
amount = reservable.amount_by_group(user.group_id).amount
|
|
|
|
if plan
|
|
|
|
# Return True if the subscription link a training credit for training reserved by the user
|
2019-09-10 11:46:14 +02:00
|
|
|
space_is_creditable = plan.training_credits.select { |credit| credit.creditable_id == reservable.id }.any?
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
# Training reserved by the user is free when :
|
2016-08-10 15:34:47 +02:00
|
|
|
|
2017-02-27 17:38:15 +01:00
|
|
|
# |-> the user already has a current subscription and if space_is_creditable is true and has at least one credit available.
|
2016-03-23 18:39:41 +01:00
|
|
|
if !new_plan_being_bought
|
2019-09-10 11:46:14 +02:00
|
|
|
amount = 0 if user.training_credits.size < plan.training_credit_nb && space_is_creditable
|
2017-02-27 17:38:15 +01:00
|
|
|
# |-> the user buys a new subscription and if space_is_creditable is true.
|
2016-03-23 18:39:41 +01:00
|
|
|
else
|
2019-09-10 11:46:14 +02:00
|
|
|
amount = 0 if space_is_creditable
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(amount, slot, admin, elements: all_elements, is_division: false)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Event reservation
|
|
|
|
when Event
|
2020-11-03 16:50:11 +01:00
|
|
|
amount = reservable.amount * options[:nb_places]
|
|
|
|
options[:tickets]&.each do |ticket|
|
2016-08-29 15:43:47 +02:00
|
|
|
amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount
|
2019-09-10 11:46:14 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(amount, slot, admin, elements: all_elements, is_division: false)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2017-02-23 17:45:55 +01:00
|
|
|
# Space reservation
|
|
|
|
when Space
|
2017-02-27 17:38:15 +01:00
|
|
|
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
|
|
|
|
|
|
if plan
|
2019-09-10 11:46:14 +02:00
|
|
|
space_credit = plan.space_credits.select { |credit| credit.creditable_id == reservable.id }.first
|
2017-02-27 17:38:15 +01:00
|
|
|
if space_credit
|
2019-09-10 11:46:14 +02:00
|
|
|
hours_available = credits_hours(space_credit, user, new_plan_being_bought)
|
2017-02-27 17:38:15 +01:00
|
|
|
slots.each_with_index do |slot, index|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements, has_credits: (index < hours_available))
|
2017-02-27 17:38:15 +01:00
|
|
|
end
|
|
|
|
else
|
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
|
2017-02-27 17:38:15 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
slots.each do |slot|
|
2020-05-05 17:56:47 +02:00
|
|
|
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
|
2017-02-27 17:38:15 +01:00
|
|
|
end
|
2017-02-23 17:45:55 +01:00
|
|
|
end
|
|
|
|
|
2019-09-11 10:31:22 +02:00
|
|
|
# No reservation (only subscription)
|
|
|
|
when nil
|
|
|
|
total_amount = 0
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
# Unknown reservation type
|
|
|
|
else
|
|
|
|
raise NotImplementedError
|
2019-09-10 11:46:14 +02:00
|
|
|
end
|
|
|
|
|
2020-11-03 16:50:11 +01:00
|
|
|
# === compute Plan price (if any) ===
|
|
|
|
unless options[:plan_id].nil?
|
2019-09-10 11:46:14 +02:00
|
|
|
all_elements[:plan] = plan.amount
|
|
|
|
total_amount += plan.amount
|
|
|
|
end
|
|
|
|
|
|
|
|
# === apply Coupon if any ===
|
|
|
|
_amount_no_coupon = total_amount
|
2020-05-06 12:43:47 +02:00
|
|
|
cs = CouponService.new
|
2020-11-03 16:50:11 +01:00
|
|
|
cp = cs.validate(options[:coupon_code], user.id)
|
2020-05-06 12:43:47 +02:00
|
|
|
total_amount = cs.apply(total_amount, cp)
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2020-11-12 16:44:55 +01:00
|
|
|
# == generate PaymentSchedule (if applicable) ===
|
2020-11-03 16:50:11 +01:00
|
|
|
schedule = if options[:payment_schedule] && plan.monthly_payment
|
2020-11-12 16:44:55 +01:00
|
|
|
PaymentScheduleService.new.compute(plan, _amount_no_coupon, cp)
|
2020-11-03 16:50:11 +01:00
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
if schedule
|
|
|
|
total_amount -= schedule[:payment_schedule].total
|
|
|
|
total_amount += schedule[:items][0].amount
|
|
|
|
end
|
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
# return result
|
2020-11-03 16:50:11 +01:00
|
|
|
{
|
|
|
|
elements: all_elements,
|
|
|
|
total: total_amount.to_i,
|
|
|
|
before_coupon: _amount_no_coupon.to_i,
|
|
|
|
coupon: cp,
|
|
|
|
schedule: schedule
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2016-08-09 16:36:14 +02:00
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
private
|
|
|
|
|
2020-05-05 17:56:47 +02:00
|
|
|
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true }.freeze
|
2019-09-10 11:46:14 +02:00
|
|
|
##
|
|
|
|
# Compute the price of a single slot, according to the base price and the ability for an admin
|
|
|
|
# to offer the slot.
|
2020-05-05 17:56:47 +02:00
|
|
|
# @param hourly_rate {Number} base price of a slot
|
2019-09-10 11:46:14 +02:00
|
|
|
# @param slot {Hash} Slot object
|
|
|
|
# @param is_admin {Boolean} true if the current user has the 'admin' role
|
2020-05-05 17:56:47 +02:00
|
|
|
# @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 an full availability, true if it is a subdivision (default)
|
2019-09-10 11:46:14 +02:00
|
|
|
# @return {Number} price of the slot
|
|
|
|
##
|
2020-05-05 17:56:47 +02:00
|
|
|
def get_slot_price(hourly_rate, slot, is_admin, options = {})
|
|
|
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
|
|
|
|
|
|
|
slot_rate = options[:has_credits] || (slot[:offered] && is_admin) ? 0 : hourly_rate
|
|
|
|
real_price = if options[:is_division]
|
|
|
|
(slot_rate / MINUTES_PER_HOUR) * ((slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE)
|
|
|
|
else
|
|
|
|
slot_rate
|
|
|
|
end
|
|
|
|
|
|
|
|
unless options[:elements].nil?
|
|
|
|
options[:elements][:slots].push(
|
|
|
|
start_at: slot[:start_at],
|
|
|
|
price: real_price,
|
|
|
|
promo: (slot_rate != hourly_rate)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
real_price
|
2019-09-10 11:46:14 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-09-10 11:46:14 +02:00
|
|
|
##
|
|
|
|
# Compute the number of remaining hours in the users current credits (for machine or space)
|
|
|
|
##
|
|
|
|
def credits_hours(credits, user, new_plan_being_bought)
|
|
|
|
hours_available = credits.hours
|
|
|
|
unless new_plan_being_bought
|
|
|
|
user_credit = user.users_credits.find_by(credit_id: credits.id)
|
|
|
|
hours_available = credits.hours - user_credit.hours_used if user_credit
|
|
|
|
end
|
|
|
|
hours_available
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|