2020-02-11 13:22:20 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-03-25 10:16:47 +01:00
|
|
|
class Reservation < ApplicationRecord
|
2016-03-23 18:39:41 +01:00
|
|
|
include NotifyWith::NotificationAttachedObject
|
|
|
|
|
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
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
accepts_nested_attributes_for :slots, allow_destroy: true
|
|
|
|
belongs_to :reservable, polymorphic: true
|
|
|
|
|
2016-08-25 18:41:33 +02:00
|
|
|
has_many :tickets
|
|
|
|
accepts_nested_attributes_for :tickets, allow_destroy: false
|
|
|
|
|
2018-12-11 17:27:25 +01:00
|
|
|
has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
validates_presence_of :reservable_id, :reservable_type
|
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) }
|
2020-02-11 13:22:20 +01:00
|
|
|
validates_with ReservationSlotSubscriptionValidator
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-09-10 17:57:46 +02:00
|
|
|
attr_accessor :plan_id, :subscription
|
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
|
2018-12-11 17:27:25 +01:00
|
|
|
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
|
2016-07-08 17:24:51 +02:00
|
|
|
after_create :debit_user_wallet
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2016-08-10 17:37:17 +02:00
|
|
|
##
|
2016-03-23 18:39:41 +01:00
|
|
|
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
|
|
|
|
# The training/machine price is depending of the member's group, subscription and credits already used
|
|
|
|
# @param on_site {Boolean} true if an admin triggered the call
|
2016-08-10 17:37:17 +02:00
|
|
|
# @param coupon_code {String} pass a valid code to appy a coupon
|
|
|
|
##
|
|
|
|
def generate_invoice_items(on_site = false, coupon_code = nil)
|
2016-03-23 18:39:41 +01:00
|
|
|
# prepare the plan
|
2018-12-11 15:07:21 +01:00
|
|
|
plan = if user.subscribed_plan
|
|
|
|
user.subscribed_plan
|
|
|
|
elsif plan_id
|
|
|
|
Plan.find(plan_id)
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2017-09-06 17:10:10 +02:00
|
|
|
# check that none of the reserved availabilities was locked
|
|
|
|
slots.each do |slot|
|
2018-12-11 15:07:21 +01:00
|
|
|
raise LockedError if slot.availability.lock
|
2017-09-06 17:10:10 +02:00
|
|
|
end
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
case reservable
|
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
# === Machine reservation ===
|
|
|
|
when Machine
|
|
|
|
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
|
|
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
2016-04-07 16:02:09 +02:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
slots.each_with_index do |slot, index|
|
|
|
|
description = reservable.name +
|
|
|
|
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
2016-04-07 16:02:09 +02:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
ii_amount = base_amount # ii_amount default to base_amount
|
2016-04-07 16:02:09 +02:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
if users_credits_manager.will_use_credits?
|
|
|
|
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
|
|
|
|
end
|
2016-04-07 16:02:09 +02:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
|
2016-04-07 16:02:09 +02:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: ii_amount,
|
|
|
|
description: description
|
|
|
|
)
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
# === Training reservation ===
|
|
|
|
when Training
|
|
|
|
base_amount = reservable.amount_by_group(user.group_id).amount
|
|
|
|
|
|
|
|
# be careful, variable plan can be the user's plan OR the plan user is currently purchasing
|
|
|
|
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
|
|
|
base_amount = 0 if users_credits_manager.will_use_credits?
|
|
|
|
|
|
|
|
slots.each do |slot|
|
|
|
|
description = reservable.name +
|
|
|
|
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
|
|
|
ii_amount = base_amount
|
|
|
|
ii_amount = 0 if slot.offered && on_site
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: ii_amount,
|
|
|
|
description: description
|
|
|
|
)
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
# === Event reservation ===
|
|
|
|
when Event
|
|
|
|
amount = reservable.amount * nb_reserve_places
|
|
|
|
tickets.each do |ticket|
|
|
|
|
amount += ticket.booked * ticket.event_price_category.amount
|
|
|
|
end
|
|
|
|
slots.each do |slot|
|
2019-10-16 16:11:37 +02:00
|
|
|
description = "#{reservable.name}\n"
|
|
|
|
description += if slot.start_at.to_date != slot.end_at.to_date
|
|
|
|
I18n.t('events.from_STARTDATE_to_ENDDATE',
|
|
|
|
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
|
|
|
|
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
|
|
|
|
I18n.t('events.from_STARTTIME_to_ENDTIME',
|
|
|
|
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
|
|
|
|
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
|
|
|
|
else
|
|
|
|
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
|
|
|
|
" - #{I18n.l slot.end_at, format: :hour_minute}"
|
|
|
|
end
|
2018-12-11 15:07:21 +01:00
|
|
|
ii_amount = amount
|
|
|
|
ii_amount = 0 if slot.offered && on_site
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: ii_amount,
|
|
|
|
description: description
|
|
|
|
)
|
|
|
|
end
|
2017-02-23 17:45:55 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
# === Space reservation ===
|
|
|
|
when Space
|
|
|
|
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
|
|
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
2017-02-23 17:45:55 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
slots.each_with_index do |slot, index|
|
|
|
|
description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
2017-02-23 17:45:55 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
ii_amount = base_amount # ii_amount default to base_amount
|
2017-02-27 17:38:15 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
if users_credits_manager.will_use_credits?
|
|
|
|
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
|
|
|
|
end
|
2017-02-27 17:38:15 +01:00
|
|
|
|
2018-12-11 17:27:25 +01:00
|
|
|
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
|
2017-02-23 17:45:55 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: ii_amount,
|
|
|
|
description: description
|
|
|
|
)
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
# === Unknown reservation type ===
|
|
|
|
else
|
|
|
|
raise NotImplementedError
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
end
|
|
|
|
|
2016-08-10 17:37:17 +02:00
|
|
|
# === Coupon ===
|
|
|
|
unless coupon_code.nil?
|
2016-11-24 17:57:48 +01:00
|
|
|
@coupon = Coupon.find_by(code: coupon_code)
|
2018-12-11 17:27:25 +01:00
|
|
|
raise InvalidCouponError if @coupon.nil? || @coupon.status(user.id) != 'active'
|
|
|
|
|
2019-01-10 16:50:54 +01:00
|
|
|
total = cart_total
|
2018-12-11 17:27:25 +01:00
|
|
|
|
|
|
|
discount = if @coupon.type == 'percent_off'
|
|
|
|
(total * @coupon.percent_off / 100).to_i
|
|
|
|
elsif @coupon.type == 'amount_off'
|
|
|
|
@coupon.amount_off
|
|
|
|
else
|
|
|
|
raise InvalidCouponError
|
|
|
|
end
|
2016-08-10 17:37:17 +02:00
|
|
|
end
|
|
|
|
|
2019-01-10 16:50:54 +01:00
|
|
|
@wallet_amount_debit = wallet_amount_debit
|
2019-09-09 17:37:54 +02:00
|
|
|
# if @wallet_amount_debit != 0 && !on_site
|
|
|
|
# invoice_items << Stripe::InvoiceItem.create(
|
|
|
|
# customer: user.stp_customer_id,
|
|
|
|
# amount: -@wallet_amount_debit.to_i,
|
|
|
|
# currency: Rails.application.secrets.stripe_currency,
|
|
|
|
# description: "wallet -#{@wallet_amount_debit / 100.0}"
|
|
|
|
# )
|
|
|
|
# end
|
|
|
|
|
|
|
|
true
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2017-06-08 19:38:19 +02:00
|
|
|
# check reservation amount total and strip invoice total to pay is equal
|
2019-06-04 16:50:23 +02:00
|
|
|
# @param stp_invoice[Stripe::Invoice]
|
|
|
|
# @param coupon_code[String]
|
2017-06-08 19:38:19 +02:00
|
|
|
# return Boolean
|
|
|
|
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil)
|
|
|
|
compute_amount_total_to_pay(coupon_code) == stp_invoice.total
|
|
|
|
end
|
|
|
|
|
|
|
|
def clear_payment_info(card, invoice)
|
2019-01-10 16:50:54 +01:00
|
|
|
card&.delete
|
|
|
|
if invoice
|
|
|
|
invoice.closed = true
|
|
|
|
invoice.save
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2019-01-10 16:50:54 +01:00
|
|
|
rescue Stripe::InvalidRequestError => e
|
|
|
|
logger.error e
|
|
|
|
rescue Stripe::AuthenticationError => e
|
|
|
|
logger.error e
|
|
|
|
rescue Stripe::APIConnectionError => e
|
|
|
|
logger.error e
|
|
|
|
rescue Stripe::StripeError => e
|
|
|
|
logger.error e
|
|
|
|
rescue StandardError => e
|
|
|
|
logger.error e
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2017-06-08 19:38:19 +02:00
|
|
|
def clean_pending_strip_invoice_items
|
|
|
|
pending_invoice_items = Stripe::InvoiceItem.list(customer: user.stp_customer_id, limit: 100).data.select { |ii| ii.invoice.nil? }
|
2019-01-10 16:50:54 +01:00
|
|
|
pending_invoice_items.each(&:delete)
|
2017-06-08 19:38:19 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-09-10 16:45:45 +02:00
|
|
|
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
2019-09-17 15:13:20 +02:00
|
|
|
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
|
|
|
|
2019-06-12 12:22:38 +02:00
|
|
|
build_invoice(
|
|
|
|
invoicing_profile: user.invoicing_profile,
|
|
|
|
statistic_profile: user.statistic_profile,
|
2019-09-10 16:45:45 +02:00
|
|
|
operator_profile_id: operator_profile_id,
|
2019-09-17 15:13:20 +02:00
|
|
|
stp_payment_intent_id: payment_intent_id,
|
|
|
|
payment_method: method
|
2019-06-12 12:22:38 +02:00
|
|
|
)
|
2019-01-14 12:45:17 +01:00
|
|
|
generate_invoice_items(true, coupon_code)
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-11 15:07:21 +01:00
|
|
|
return false unless valid?
|
|
|
|
|
|
|
|
if plan_id
|
2019-06-04 16:50:23 +02:00
|
|
|
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
|
|
|
|
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
|
2019-09-10 11:46:14 +02:00
|
|
|
if subscription.save_with_payment(operator_profile_id, false)
|
2018-12-11 15:07:21 +01:00
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: subscription.plan.amount,
|
|
|
|
description: subscription.plan.name,
|
|
|
|
subscription_id: subscription.id
|
|
|
|
)
|
2016-08-10 16:33:26 +02:00
|
|
|
set_total_and_coupon(coupon_code)
|
2016-03-23 18:39:41 +01:00
|
|
|
save!
|
2018-12-11 15:07:21 +01:00
|
|
|
else
|
|
|
|
errors[:card] << subscription.errors[:card].join
|
|
|
|
return false
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2018-12-11 15:07:21 +01:00
|
|
|
else
|
|
|
|
set_total_and_coupon(coupon_code)
|
|
|
|
save!
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2018-12-11 15:07:21 +01:00
|
|
|
|
|
|
|
UsersCredits::Manager.new(reservation: self).update_credits
|
|
|
|
true
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
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:
|
|
|
|
# - machine/training/space reservation => 1 slot = 1 seat (currently not covered by this function)
|
|
|
|
# - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
|
2019-11-25 10:45:54 +01:00
|
|
|
return 0 if slots.first.canceled_at && !canceled
|
|
|
|
|
2019-11-25 10:30:03 +01:00
|
|
|
total = nb_reserve_places
|
2019-01-10 16:50:54 +01:00
|
|
|
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
|
|
|
|
|
2016-08-30 09:47:03 +02:00
|
|
|
total
|
|
|
|
end
|
|
|
|
|
2019-06-04 16:50:23 +02:00
|
|
|
def user
|
|
|
|
statistic_profile.user
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
already_reserved = false
|
2019-01-10 16:50:54 +01:00
|
|
|
slots.each do |slot|
|
2017-02-28 13:23:31 +01:00
|
|
|
same_hour_slots = Slot.joins(:reservations).where(
|
2019-01-10 16:50:54 +01:00
|
|
|
reservations: { reservable_type: reservable_type, reservable_id: reservable_id },
|
|
|
|
start_at: slot.start_at,
|
|
|
|
end_at: slot.end_at,
|
|
|
|
availability_id: slot.availability_id,
|
|
|
|
canceled_at: nil
|
|
|
|
)
|
2016-03-23 18:39:41 +01:00
|
|
|
if same_hour_slots.any?
|
|
|
|
already_reserved = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2019-01-10 16:50:54 +01:00
|
|
|
errors.add(:machine, 'already reserved') if already_reserved
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def training_not_fully_reserved
|
2019-01-10 16:50:54 +01:00
|
|
|
slot = slots.first
|
|
|
|
errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed?
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def notify_member_create_reservation
|
|
|
|
NotificationCenter.call type: 'notify_member_create_reservation',
|
|
|
|
receiver: user,
|
|
|
|
attached_object: self
|
|
|
|
end
|
|
|
|
|
|
|
|
def notify_admin_member_create_reservation
|
|
|
|
NotificationCenter.call type: 'notify_admin_member_create_reservation',
|
|
|
|
receiver: User.admins,
|
|
|
|
attached_object: self
|
|
|
|
end
|
|
|
|
|
2019-01-10 16:50:54 +01:00
|
|
|
def cart_total
|
|
|
|
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
2016-07-08 17:24:51 +02:00
|
|
|
if plan_id.present?
|
2016-07-21 12:57:13 +02:00
|
|
|
plan = Plan.find(plan_id)
|
2016-07-08 17:24:51 +02:00
|
|
|
total += plan.amount
|
|
|
|
end
|
2016-11-29 11:12:58 +01:00
|
|
|
total
|
|
|
|
end
|
|
|
|
|
2019-01-10 16:50:54 +01:00
|
|
|
def wallet_amount_debit
|
|
|
|
total = cart_total
|
|
|
|
total = CouponService.new.apply(total, @coupon, user.id) if @coupon
|
|
|
|
|
2016-07-08 17:24:51 +02:00
|
|
|
wallet_amount = (user.wallet.amount * 100).to_i
|
2016-08-10 15:34:47 +02:00
|
|
|
|
|
|
|
wallet_amount >= total ? total : wallet_amount
|
2016-07-08 17:24:51 +02:00
|
|
|
end
|
|
|
|
|
2016-07-07 15:57:06 +02:00
|
|
|
def debit_user_wallet
|
2019-01-10 16:50:54 +01:00
|
|
|
return unless @wallet_amount_debit.present? && @wallet_amount_debit != 0
|
|
|
|
|
|
|
|
amount = @wallet_amount_debit / 100.0
|
|
|
|
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, self)
|
|
|
|
# wallet debit success
|
|
|
|
raise DebitWalletError unless wallet_transaction
|
|
|
|
|
2019-04-08 17:03:43 +02:00
|
|
|
invoice.set_wallet_transaction(@wallet_amount_debit, wallet_transaction.id)
|
2016-07-07 15:57:06 +02:00
|
|
|
end
|
2016-08-10 16:33:26 +02:00
|
|
|
|
2017-06-08 19:38:19 +02:00
|
|
|
# this function only use for compute total of reservation before save
|
|
|
|
def compute_amount_total_to_pay(coupon_code = nil)
|
|
|
|
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
|
|
|
unless coupon_code.nil?
|
|
|
|
cp = Coupon.find_by(code: coupon_code)
|
2019-01-10 16:50:54 +01:00
|
|
|
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
|
|
|
|
|
|
|
|
total = CouponService.new.apply(total, cp, user.id)
|
2017-06-08 19:38:19 +02:00
|
|
|
end
|
2019-01-10 16:50:54 +01:00
|
|
|
total - wallet_amount_debit
|
2017-06-08 19:38:19 +02:00
|
|
|
end
|
|
|
|
|
2016-08-10 16:33:26 +02:00
|
|
|
##
|
|
|
|
# Set the total price to the reservation's invoice, summing its whole items.
|
|
|
|
# Additionally a coupon may be applied to this invoice to make a discount on the total price
|
|
|
|
# @param [coupon_code] {String} optional coupon code to apply to the invoice
|
|
|
|
##
|
|
|
|
def set_total_and_coupon(coupon_code = nil)
|
|
|
|
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
|
|
|
|
|
|
|
unless coupon_code.nil?
|
2016-11-23 16:30:19 +01:00
|
|
|
cp = Coupon.find_by(code: coupon_code)
|
2019-01-10 16:50:54 +01:00
|
|
|
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
|
|
|
|
|
|
|
|
total = CouponService.new.apply(total, cp, user.id)
|
|
|
|
invoice.coupon_id = cp.id
|
2016-08-10 16:33:26 +02:00
|
|
|
end
|
|
|
|
|
2019-01-10 16:50:54 +01:00
|
|
|
invoice.total = total
|
2016-08-10 16:33:26 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|