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:
parent
c3d6206dba
commit
8504864c20
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
63
app/services/reservation_limit_service.rb
Normal file
63
app/services/reservation_limit_service.rb
Normal 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
|
@ -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}"
|
||||
|
127
test/services/reservation_limit_service_test.rb
Normal file
127
test/services/reservation_limit_service_test.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user