# 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