1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00
fab-manager/app/models/reservation.rb
2019-03-18 11:11:09 +01:00

523 lines
18 KiB
Ruby

class Reservation < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :user
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) }
attr_accessor :card_token, :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.
# 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
# @param coupon_code {String} pass a valid code to appy a coupon
##
def generate_invoice_items(on_site = false, coupon_code = nil)
# returning array
invoice_items = []
# prepare the plan
plan = if user.subscribed_plan
user.subscribed_plan
elsif plan_id
Plan.find(plan_id)
else
nil
end
# check that none of the reserved availabilities was locked
slots.each do |slot|
raise LockedError if slot.availability.lock
end
case reservable
# === 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)
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}"
ii_amount = base_amount # ii_amount default to base_amount
if users_credits_manager.will_use_credits?
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
end
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
unless on_site # if it's local payment then do not create Stripe::InvoiceItem
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
stp_invoice_item_id: (ii.id if ii),
description: description
)
end
# === 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
unless on_site
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
stp_invoice_item_id: (ii.id if ii),
description: description
)
end
# === 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|
description = "#{reservable.name} "
(slot.start_at.to_date..slot.end_at.to_date).each do |d|
description += "\n" if slot.start_at.to_date != slot.end_at.to_date
description += "#{I18n.l d, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
" - #{I18n.l slot.end_at, format: :hour_minute}"
end
ii_amount = amount
ii_amount = 0 if slot.offered && on_site
unless on_site
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
stp_invoice_item_id: (ii.id if ii),
description: description
)
end
# === 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)
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}"
ii_amount = base_amount # ii_amount default to base_amount
if users_credits_manager.will_use_credits?
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
end
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
unless on_site # if it's local payment then do not create Stripe::InvoiceItem
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
stp_invoice_item_id: (ii.id if ii),
description: description
)
end
# === Unknown reservation type ===
else
raise NotImplementedError
end
# === Coupon ===
unless coupon_code.nil?
@coupon = Coupon.find_by(code: coupon_code)
raise InvalidCouponError if @coupon.nil? || @coupon.status(user.id) != 'active'
total = cart_total
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
unless on_site
invoice_items << Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: -discount,
currency: Rails.application.secrets.stripe_currency,
description: "coupon #{@coupon.code}"
)
end
end
@wallet_amount_debit = wallet_amount_debit
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
# let's return the resulting array of items
invoice_items
end
def save_with_payment(operator_id, coupon_code = nil)
begin
clean_pending_strip_invoice_items
build_invoice(user: user, operator_id: operator_id)
invoice_items = generate_invoice_items(false, coupon_code)
rescue StandardError => e
logger.error e
errors[:payment] << e.message
return false
end
return false unless valid?
# TODO: refactoring
customer = Stripe::Customer.retrieve(user.stp_customer_id)
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, card_token: card_token, expiration_date: nil }
if subscription.save_with_payment(operator_id, false)
self.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount,
stp_invoice_item_id: subscription.stp_subscription_id,
description: subscription.plan.name,
subscription_id: subscription.id
)
set_total_and_coupon(coupon_code)
save!
#
# IMPORTANT NOTE: here, we don't have to create a stripe::invoice and pay it
# because subscription.create (in subscription.rb) will pay all waiting stripe invoice items
#
else
# error handling
invoice_items.each(&:delete)
errors[:card] << subscription.errors[:card].join
errors[:payment] << subscription.errors[:payment].join if subscription.errors[:payment]
return false
end
else
begin
if invoice_items.map(&:amount).map(&:to_i).reduce(:+).positive?
card = customer.sources.create(card: card_token)
if customer.default_source.present?
customer.default_source = card.id
customer.save
end
end
#
# IMPORTANT NOTE: here, we have to create an invoice manually and pay it to pay all waiting stripe invoice items
#
stp_invoice = Stripe::Invoice.create(
customer: user.stp_customer_id,
)
# cf: https://board.sleede.com/project/sleede-fab-manager/issue/77
# this function only check reservation total is equal strip invoice total when
# pay only reservation not reservation + subscription
# if !is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code)
# #raise InvoiceTotalDifferentError
# end
stp_invoice.pay
card&.delete
self.stp_invoice_id = stp_invoice.id
invoice.stp_invoice_id = stp_invoice.id
set_total_and_coupon(coupon_code)
save!
rescue Stripe::CardError => card_error
clear_payment_info(card, stp_invoice)
logger.info card_error
errors[:card] << card_error.message
return false
rescue Stripe::InvalidRequestError => e
# Invalid parameters were supplied to Stripe's API
clear_payment_info(card, stp_invoice)
logger.error e
errors[:payment] << e.message
return false
rescue Stripe::AuthenticationError => e
# Authentication with Stripe's API failed
# (maybe you changed API keys recently)
clear_payment_info(card, stp_invoice)
logger.error e
errors[:payment] << e.message
return false
rescue Stripe::APIConnectionError => e
# Network communication with Stripe failed
clear_payment_info(card, stp_invoice)
logger.error e
errors[:payment] << e.message
return false
rescue Stripe::StripeError => e
# Display a very generic error to the user, and maybe send
# yourself an email
clear_payment_info(card, stp_invoice)
logger.error e
errors[:payment] << e.message
return false
rescue StandardError => e
# Something else happened, completely unrelated to Stripe
clear_payment_info(card, stp_invoice)
logger.error e
errors[:payment] << e.message
return false
end
end
UsersCredits::Manager.new(reservation: self).update_credits
true
end
# check reservation amount total and strip invoice total to pay is equal
# @params stp_invoice[Stripe::Invoice]
# @params 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_local_payment(operator_id, coupon_code = nil)
build_invoice(user: user, operator_id: operator_id)
generate_invoice_items(true, coupon_code)
return false unless valid?
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, expiration_date: nil }
if subscription.save_with_local_payment(operator_id, false)
invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount,
description: subscription.plan.name,
subscription_id: subscription.id
)
set_total_and_coupon(coupon_code)
save!
else
errors[:card] << subscription.errors[:card].join
return false
end
else
set_total_and_coupon(coupon_code)
save!
end
UsersCredits::Manager.new(reservation: self).update_credits
true
end
def total_booked_seats
total = nb_reserve_places
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
total
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
private
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
def update_event_nb_free_places
if reservable_id_was.blank?
# simple reservation creation, we subtract the number of booked seats from the previous number
nb_free_places = reservable.nb_free_places - total_booked_seats
else
# reservation moved from another date (for recurring events)
seats = total_booked_seats
reservable_was = Event.find(reservable_id_was)
nb_free_places = reservable_was.nb_free_places + seats
reservable_was.update_columns(nb_free_places: nb_free_places)
nb_free_places = reservable.nb_free_places - seats
end
reservable.update_columns(nb_free_places: nb_free_places)
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.update_columns(wallet_amount: @wallet_amount_debit, wallet_transaction_id: 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_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?
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)
invoice.coupon_id = cp.id
end
invoice.total = total
end
end