1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) limit reservation depending on current suscription

This commit is contained in:
Sylvain 2023-03-13 17:29:06 +01:00
parent c3d6206dba
commit 8504864c20
7 changed files with 243 additions and 29 deletions

View File

@ -61,40 +61,20 @@ class CartItem::Reservation < CartItem::BaseItem
# @return [Boolean]
def valid?(all_items = [])
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) }
plan = pending_subscription&.plan || customer&.subscribed_plan
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
reservation_deadline = reservation_deadline_minutes.minutes.since
cart_item_reservation_slots.each do |sr|
slot = sr.slot
if slot.nil?
errors.add(:slot, I18n.t('cart_item_validation.slot'))
return false
end
availability = slot.availability
if availability.nil?
errors.add(:availability, I18n.t('cart_item_validation.availability'))
return false
end
if slot.full?
errors.add(:slot, I18n.t('cart_item_validation.full'))
return false
end
if slot.start_at < reservation_deadline && !operator.privileged?
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
return false
end
next if availability.plan_ids.empty?
next if required_subscription?(availability, pending_subscription)
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
unless ReservationLimitService.authorized?(plan, customer, self, all_items)
errors.add(:reservation, I18n.t('cart_item_validation.limit_reached', { HOURS: ReservationLimitService.limit(plan, reservable) }))
return false
end
cart_item_reservation_slots.each do |sr|
return false unless validate_slot_reservation(sr, pending_subscription, reservation_deadline, errors)
end
true
end
@ -263,4 +243,40 @@ class CartItem::Reservation < CartItem::BaseItem
(operator.manager? && customer.id != operator.id) ||
operator.admin?
end
# @param reservation_slot [CartItem::ReservationSlot]
# @param pending_subscription [CartItem::Subscription, NilClass]
# @param reservation_deadline [Date,Time]
# @param errors [ActiveModel::Errors]
# @return [Boolean]
def validate_slot_reservation(reservation_slot, pending_subscription, reservation_deadline, errors)
slot = reservation_slot.slot
if slot.nil?
errors.add(:slot, I18n.t('cart_item_validation.slot'))
return false
end
availability = slot.availability
if availability.nil?
errors.add(:availability, I18n.t('cart_item_validation.availability'))
return false
end
if slot.full?
errors.add(:slot, I18n.t('cart_item_validation.full'))
return false
end
if slot.start_at < reservation_deadline && !operator.privileged?
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
return false
end
unless availability.plan_ids.empty? && required_subscription?(availability, pending_subscription)
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
return false
end
true
end
end

View File

@ -42,7 +42,7 @@ class Machine < ApplicationRecord
belongs_to :machine_category
has_many :plan_limitations, dependent: :destroy
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
after_create :create_statistic_subtype
after_create :create_machine_prices

View File

@ -4,5 +4,5 @@
class MachineCategory < ApplicationRecord
has_many :machines, dependent: :nullify
accepts_nested_attributes_for :machines, allow_destroy: true
has_many :plan_limitations, dependent: :destroy
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine_category, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
end

View File

@ -10,4 +10,11 @@ class PlanLimitation < ApplicationRecord
validates :limitable_id, :limitable_type, :limit, :plan_id, presence: true
validates :limitable_id, uniqueness: { scope: %i[limitable_type plan_id] }
# @return [Array<Machine,Event,Space,Training>]
def reservables
return limitable.machines if limitable_type == 'MachineCategory'
[limitable]
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
# Check if a user if allowed to book a reservation without exceeding the limits set by his plan
class ReservationLimitService
class << self
# @param plan [Plan,NilClass]
# @param customer [User]
# @param reservation [CartItem::Reservation]
# @param cart_items [Array<CartItem::BaseItem>]
# @return [Boolean]
def authorized?(plan, customer, reservation, cart_items)
return true if plan.nil? || !plan.limiting
return true if reservation.nil? || !reservation.is_a?(CartItem::Reservation)
plan.plan_limitations.filter { |limit| limit.reservables.include?(reservation.reservable) }.each do |limit|
reservation.cart_item_reservation_slots.group_by { |sr| sr.slot.start_at.to_date }.each_pair do |date, reservation_slots|
daily_duration = reservations_duration(customer, date, reservation, cart_items) +
(reservation_slots.map { |sr| sr.slot.duration }.reduce(:+) || 0)
return false if Rational(daily_duration / 3600).to_f > limit.limit
end
end
true
end
# @param plan [Plan,NilClass]
# @param reservable [Machine,Event,Space,Training]
# @return [Integer,NilClass] in hours
def limit(plan, reservable)
return nil unless plan&.limiting
plan&.plan_limitations&.find { |limit| limit.reservables.include?(reservable) }&.limit
end
private
# @param customer [User]
# @param date [Date]
# @param reservation [CartItem::Reservation]
# @param cart_items [Array<CartItem::BaseItem>]
# @return [Integer] in seconds
def reservations_duration(customer, date, reservation, cart_items)
daily_reservations = customer.reservations
.includes(slots_reservations: :slot)
.where(reservable: reservation.reservable)
.where(slots_reservations: { canceled_at: nil })
.where("date_trunc('day', slots.start_at) = :date", date: date)
cart_daily_reservations = cart_items.filter do |item|
item.is_a?(CartItem::Reservation) &&
item != reservation &&
item.reservable == reservation.reservable &&
item.cart_item_reservation_slots
.includes(:slot)
.where("date_trunc('day', slots.start_at) = :date", date: date)
end
(daily_reservations.map { |r| r.slots_reservations.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0) +
(cart_daily_reservations.map { |r| r.cart_item_reservation_slots.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0)
end
end
end

View File

@ -519,6 +519,7 @@ en:
availability: "The availaility doesn't exist"
full: "The slot is already fully reserved"
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
limit_reached: "You have reached the booking limit of %{HOURS}H per day for your current subscription"
restricted: "This availability is restricted for subscribers"
plan: "This subscription plan is disabled"
plan_group: "This subscription plan is reserved for members of group %{GROUP}"

View File

@ -0,0 +1,127 @@
# frozen_string_literal: true
require 'test_helper'
class ReservationLimitServiceTest < ActiveSupport::TestCase
setup do
@acamus = User.find_by(username: 'acamus')
@admin = User.find_by(username: 'admin')
@machine = Machine.first
@plan = Plan.find(1)
end
test 'simple reservation without plan' do
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
)
assert ReservationLimitService.authorized?(nil, @acamus, reservation, [])
end
test 'simple reservation with not limiting plan' do
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
)
assert ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
end
test 'simple reservation with limiting plan' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot_id: @machine.availabilities.first.slots.first.id }]
)
assert ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
end
test 'reservation exceeds plan limit' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
slots = Availabilities::AvailabilitiesService.new(@acamus)
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[0] }, { slot: slots[1] }, { slot: slots[2] }]
)
assert_not ReservationLimitService.authorized?(@plan, @acamus, reservation, [])
end
test 'second reservation at plan limit' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
slots = Availabilities::AvailabilitiesService.new(@acamus)
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[0] }]
)
reservation2 = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[1] }]
)
assert ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
end
test 'second reservation exceeds plan limit' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
slots = Availabilities::AvailabilitiesService.new(@acamus)
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
reservation = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[0] }, { slot: slots[1] }]
)
reservation2 = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[2] }]
)
assert_not ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
end
test 'reservation of other resource should not conflict' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
slots = Availabilities::AvailabilitiesService.new(@acamus)
.machines([@machine], @acamus, { start: Time.current, end: 10.days.from_now })
reservation = CartItem::SpaceReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: Space.first,
cart_item_reservation_slots_attributes: [{ slot: Space.first.availabilities.first.slots.first },
{ slot: Space.first.availabilities.first.slots.last }]
)
reservation2 = CartItem::MachineReservation.new(
customer_profile: @acamus.invoicing_profile,
operator_profile: @acamus.invoicing_profile,
reservable: @machine,
cart_item_reservation_slots_attributes: [{ slot: slots[0] }]
)
assert ReservationLimitService.authorized?(@plan, @acamus, reservation2, [reservation])
end
test 'get plan limit' do
@plan.update(limiting: true, plan_limitations_attributes: [{ limitable_id: @machine.id, limitable_type: 'Machine', limit: 2 }])
assert_equal 2, ReservationLimitService.limit(@plan, @machine)
end
test 'get plan without limit' do
assert_nil ReservationLimitService.limit(@plan, @machine)
end
end