2019-01-09 12:07:31 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Provides methods for accessing Invoices resources and properties
|
|
|
|
class InvoicesService
|
|
|
|
# return a paginated list of invoices, ordered by the given criterion and optionally filtered
|
|
|
|
# @param order_key {string} any column from invoices or joined a table
|
|
|
|
# @param direction {string} 'ASC' or 'DESC', linked to order_key
|
|
|
|
# @param page {number} page number, used to paginate results
|
|
|
|
# @param size {number} number of items per page
|
|
|
|
# @param filters {Hash} allowed filters: number, customer, date.
|
|
|
|
def self.list(order_key, direction, page, size, filters = {})
|
2019-05-28 16:49:36 +02:00
|
|
|
invoices = Invoice.includes(:avoir, :invoicing_profile, invoice_items: %i[subscription invoice_item])
|
|
|
|
.joins(:invoicing_profile)
|
2019-01-09 12:07:31 +01:00
|
|
|
.order("#{order_key} #{direction}")
|
|
|
|
.page(page)
|
|
|
|
.per(size)
|
|
|
|
|
|
|
|
|
|
|
|
if filters[:number].size.positive?
|
|
|
|
invoices = invoices.where(
|
|
|
|
'invoices.reference LIKE :search',
|
|
|
|
search: "#{filters[:number]}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
if filters[:customer].size.positive?
|
|
|
|
# ILIKE => PostgreSQL case-insensitive LIKE
|
|
|
|
invoices = invoices.where(
|
2019-05-28 16:49:36 +02:00
|
|
|
'invoicing_profiles.first_name ILIKE :search OR invoicing_profiles.last_name ILIKE :search',
|
2019-01-09 12:07:31 +01:00
|
|
|
search: "%#{filters[:customer]}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
unless filters[:date].nil?
|
|
|
|
invoices = invoices.where(
|
|
|
|
"date_trunc('day', invoices.created_at) = :search",
|
|
|
|
search: "%#{DateTime.iso8601(filters[:date]).to_time.to_date}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
invoices
|
|
|
|
end
|
|
|
|
|
|
|
|
# Parse the order_by clause provided by JS client from '-column' form to SQL compatible form
|
|
|
|
# @param order_by {string} expected form: 'column' or '-column'
|
|
|
|
def self.parse_order(order_by)
|
|
|
|
direction = (order_by[0] == '-' ? 'DESC' : 'ASC')
|
|
|
|
key = (order_by[0] == '-' ? order_by[1, order_by.size] : order_by)
|
|
|
|
|
|
|
|
order_key = case key
|
|
|
|
when 'reference'
|
|
|
|
'invoices.reference'
|
|
|
|
when 'date'
|
|
|
|
'invoices.created_at'
|
|
|
|
when 'total'
|
|
|
|
'invoices.total'
|
|
|
|
when 'name'
|
|
|
|
'profiles.first_name'
|
|
|
|
else
|
|
|
|
'invoices.id'
|
|
|
|
end
|
|
|
|
{ direction: direction, order_key: order_key }
|
|
|
|
end
|
2020-12-16 18:33:43 +01:00
|
|
|
|
|
|
|
##
|
2020-12-21 12:02:51 +01:00
|
|
|
# Create an Invoice with an associated array of InvoiceItem matching the given parameters
|
2021-04-23 12:52:06 +02:00
|
|
|
# @param payment_details {Hash} as generated by ShoppingCart.total
|
2020-12-16 18:33:43 +01:00
|
|
|
# @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself)
|
2021-06-28 18:17:11 +02:00
|
|
|
# @param objects {Array<Reservation|Subscription|StatisticProfilePrepaidPack>} the booked reservation and/or subscription or pack
|
2021-05-28 17:34:20 +02:00
|
|
|
# @param user {User} the customer
|
2021-04-15 17:01:52 +02:00
|
|
|
# @param payment_id {String} ID of the payment, a returned by the gateway, if the current invoice is paid by card
|
|
|
|
# @param payment_method {String} the payment method used
|
2020-12-16 18:33:43 +01:00
|
|
|
##
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.create(payment_details, operator_profile_id, objects, user, payment_id: nil, payment_type: nil, payment_method: nil)
|
2020-12-16 18:33:43 +01:00
|
|
|
operator = InvoicingProfile.find(operator_profile_id)&.user
|
2021-04-15 17:01:52 +02:00
|
|
|
method = if payment_method
|
|
|
|
payment_method
|
|
|
|
else
|
2021-04-26 12:00:07 +02:00
|
|
|
operator&.admin? || (operator&.manager? && operator != user) ? nil : 'card'
|
2021-04-15 17:01:52 +02:00
|
|
|
end
|
2020-12-16 18:33:43 +01:00
|
|
|
|
|
|
|
invoice = Invoice.new(
|
|
|
|
invoicing_profile: user.invoicing_profile,
|
|
|
|
statistic_profile: user.statistic_profile,
|
|
|
|
operator_profile_id: operator_profile_id,
|
|
|
|
payment_method: method
|
|
|
|
)
|
2021-04-26 12:00:07 +02:00
|
|
|
unless payment_id.nil?
|
|
|
|
invoice.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
|
|
|
|
end
|
2020-12-16 18:33:43 +01:00
|
|
|
|
2021-05-28 17:34:20 +02:00
|
|
|
InvoicesService.generate_invoice_items(invoice, payment_details, objects)
|
2020-12-16 18:33:43 +01:00
|
|
|
InvoicesService.set_total_and_coupon(invoice, user, payment_details[:coupon])
|
|
|
|
invoice
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
2020-12-21 12:02:51 +01:00
|
|
|
# Generate an array of {InvoiceItem} with the elements in provided reservation, price included.
|
2020-12-16 18:33:43 +01:00
|
|
|
# @param invoice {Invoice} the parent invoice
|
2021-04-23 12:52:06 +02:00
|
|
|
# @param payment_details {Hash} as generated by ShoppingCart.total
|
2021-06-28 18:17:11 +02:00
|
|
|
# @param objects {Array<Reservation|Subscription|StatisticProfilePrepaidPack>}
|
2020-12-16 18:33:43 +01:00
|
|
|
##
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.generate_invoice_items(invoice, payment_details, objects)
|
|
|
|
objects.each_with_index do |object, index|
|
|
|
|
if object.is_a?(Reservation) && object.reservable.is_a?(Event)
|
|
|
|
InvoicesService.generate_event_item(invoice, object, payment_details, index.zero?)
|
|
|
|
elsif object.is_a?(Subscription)
|
|
|
|
InvoicesService.generate_subscription_item(invoice, object, payment_details, index.zero?)
|
|
|
|
elsif object.is_a?(Reservation)
|
|
|
|
InvoicesService.generate_reservation_item(invoice, object, payment_details, index.zero?)
|
2021-06-28 18:17:11 +02:00
|
|
|
elsif object.is_a?(StatisticProfilePrepaidPack)
|
|
|
|
InvoicesService.generate_prepaid_pack_item(invoice, object, payment_details, index.zero?)
|
2020-12-16 18:33:43 +01:00
|
|
|
else
|
2021-05-28 17:34:20 +02:00
|
|
|
InvoicesService.generate_generic_item(invoice, object, payment_details, index.zero?)
|
2020-12-16 18:33:43 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
2020-12-21 12:02:51 +01:00
|
|
|
# Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
|
2020-12-16 18:33:43 +01:00
|
|
|
# This method must be called if reservation.reservable is an Event
|
|
|
|
##
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.generate_event_item(invoice, reservation, payment_details, main = false)
|
2021-04-26 11:40:26 +02:00
|
|
|
raise TypeError unless reservation.reservable.is_a? Event
|
2020-12-16 18:33:43 +01:00
|
|
|
|
|
|
|
reservation.slots.each do |slot|
|
|
|
|
description = "#{reservation.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],
|
2021-05-27 15:58:55 +02:00
|
|
|
description: description,
|
|
|
|
object: reservation,
|
2021-05-28 17:34:20 +02:00
|
|
|
main: main
|
2020-12-16 18:33:43 +01:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
2020-12-21 12:02:51 +01:00
|
|
|
# Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
|
2020-12-16 18:33:43 +01:00
|
|
|
# This method must be called if reservation.reservable is a Space, a Machine or a Training
|
|
|
|
##
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.generate_reservation_item(invoice, reservation, payment_details, main = false)
|
2020-12-16 18:33:43 +01:00
|
|
|
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
|
|
|
|
|
|
|
|
reservation.slots.each do |slot|
|
|
|
|
description = reservation.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],
|
2021-05-27 15:58:55 +02:00
|
|
|
description: description,
|
|
|
|
object: reservation,
|
2021-05-28 17:34:20 +02:00
|
|
|
main: main
|
2020-12-16 18:33:43 +01:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
2020-12-21 12:02:51 +01:00
|
|
|
# Generate an InvoiceItem for the given subscription and save it in invoice.invoice_items.
|
2020-12-16 18:33:43 +01:00
|
|
|
# This method must be called only with a valid subscription
|
|
|
|
##
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.generate_subscription_item(invoice, subscription, payment_details, main = false)
|
2020-12-16 18:33:43 +01:00
|
|
|
raise TypeError unless subscription
|
|
|
|
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: payment_details[:elements][:plan],
|
|
|
|
description: subscription.plan.name,
|
2021-05-27 15:58:55 +02:00
|
|
|
object: subscription,
|
|
|
|
main: main
|
2020-12-16 18:33:43 +01:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-06-28 18:17:11 +02:00
|
|
|
##
|
|
|
|
# Generate an InvoiceItem for the given StatisticProfilePrepaidPack and save it in invoice.invoice_items.
|
|
|
|
# This method must be called only with a valid pack-statistic_profile relation
|
|
|
|
##
|
|
|
|
def self.generate_prepaid_pack_item(invoice, pack, payment_details, main = false)
|
|
|
|
raise TypeError unless pack
|
|
|
|
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: payment_details[:elements][:pack],
|
2021-06-30 10:53:05 +02:00
|
|
|
description: I18n.t('invoices.pack_item', COUNT: pack.prepaid_pack.minutes / 60, ITEM: pack.prepaid_pack.priceable.name),
|
2021-06-28 18:17:11 +02:00
|
|
|
object: pack,
|
|
|
|
main: main
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-05-28 17:34:20 +02:00
|
|
|
def self.generate_generic_item(invoice, item, payment_details, main = false)
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: payment_details[:elements][item.class.name.to_sym],
|
|
|
|
description: item.class.name,
|
|
|
|
object: item,
|
|
|
|
main: main
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2020-12-16 18:33:43 +01: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 invoice {Invoice} the invoice to fill
|
|
|
|
# @param user {User} the customer
|
|
|
|
# @param [coupon] {Coupon} optional coupon to apply to the invoice
|
|
|
|
##
|
|
|
|
def self.set_total_and_coupon(invoice, user, coupon = nil)
|
|
|
|
return unless invoice
|
|
|
|
|
|
|
|
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
|
2019-01-09 12:07:31 +01:00
|
|
|
end
|