1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00
fab-manager/app/models/reservation.rb

276 lines
9.5 KiB
Ruby

# frozen_string_literal: true
# Reservation is a Slot or a Ticket booked by a member.
# Slots are for Machine, Space and Training reservations.
# Tickets are for Event reservations.
class Reservation < ApplicationRecord
include NotifyWith::NotificationAttachedObject
belongs_to :statistic_profile
has_many :slots_reservations, dependent: :destroy
has_many :slots, through: :slots_reservations
accepts_nested_attributes_for :slots, allow_destroy: true
belongs_to :reservable, polymorphic: true
has_many :tickets
accepts_nested_attributes_for :tickets, allow_destroy: false
has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
validates_presence_of :reservable_id, :reservable_type
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
validates_with ReservationSlotSubscriptionValidator
attr_accessor :plan_id, :subscription
after_commit :notify_member_create_reservation, on: :create
after_commit :notify_admin_member_create_reservation, on: :create
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
after_create :debit_user_wallet
##
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
# @param payment_details {Hash} as generated by Price.compute
##
def generate_invoice_items(payment_details = nil)
# check that none of the reserved availabilities was locked
slots.each do |slot|
raise LockedError if slot.availability.lock
end
case reservable
# === Event reservation ===
when Event
slots.each do |slot|
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
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
# === Space|Machine|Training reservation ===
else
slots.each do |slot|
description = reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
end
# === Coupon ===
@coupon = payment_details[:coupon]
# === Wallet ===
@wallet_amount_debit = wallet_amount_debit
end
# check reservation amount total and strip invoice total to pay is equal
# @param stp_invoice[Stripe::Invoice]
# @param coupon_code[String]
# 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)
card&.delete
if invoice
invoice.closed = true
invoice.save
end
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
end
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? }
pending_invoice_items.each(&:delete)
end
def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
build_invoice(
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id,
stp_payment_intent_id: payment_intent_id,
payment_method: method
)
generate_invoice_items(payment_details)
return false unless valid?
if plan_id
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 }
if subscription.save_with_payment(operator_profile_id, false)
invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:elements][:plan],
description: subscription.plan.name,
subscription_id: subscription.id
)
set_total_and_coupon(payment_details[:coupon])
save!
else
errors[:card] << subscription.errors[:card].join
return false
end
else
set_total_and_coupon(payment_details[:coupon])
save!
end
UsersCredits::Manager.new(reservation: self).update_credits
true
end
# @param canceled if true, count the number of seats for this reservation, including canceled seats
def total_booked_seats(canceled: false)
# 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)
return 0 if slots.first.canceled_at && !canceled
total = nb_reserve_places
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
total
end
def user
statistic_profile.user
end
def update_event_nb_free_places
return unless reservable_type == 'Event'
reservable.update_nb_free_places
reservable.save!
end
private
def machine_not_already_reserved
already_reserved = false
slots.each do |slot|
same_hour_slots = Slot.joins(:reservations).where(
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
)
if same_hour_slots.any?
already_reserved = true
break
end
end
errors.add(:machine, 'already reserved') if already_reserved
end
def training_not_fully_reserved
slot = slots.first
errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed?
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_and_managers,
attached_object: self
end
def cart_total
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
if plan_id.present?
plan = Plan.find(plan_id)
total += plan.amount
end
total
end
def wallet_amount_debit
total = cart_total
total = CouponService.new.apply(total, @coupon, user.id) if @coupon
wallet_amount = (user.wallet.amount * 100).to_i
wallet_amount >= total ? total : wallet_amount
end
def debit_user_wallet
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
invoice.set_wallet_transaction(@wallet_amount_debit, wallet_transaction.id)
end
# 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)
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
total = CouponService.new.apply(total, cp, user.id)
end
total - wallet_amount_debit
end
##
# 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] {Coupon} optional coupon to apply to the invoice
##
def set_total_and_coupon(coupon = nil)
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
unless coupon.nil?
total = CouponService.new.apply(total, coupon, user.id)
invoice.coupon_id = coupon.id
end
invoice.total = total
end
end