2020-11-03 16:50:11 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# create PaymentSchedules for various items
|
|
|
|
class PaymentScheduleService
|
|
|
|
##
|
|
|
|
# Compute a payment schedule for a new subscription to the provided plan
|
|
|
|
# @param plan {Plan}
|
2020-11-05 14:55:06 +01:00
|
|
|
# @param total {Number} Total amount of the current shopping cart (which includes this plan) - without coupon
|
2020-11-03 16:50:11 +01:00
|
|
|
# @param coupon {Coupon} apply this coupon, if any
|
|
|
|
##
|
2021-01-04 17:03:44 +01:00
|
|
|
def compute(plan, total, coupon: nil, subscription: nil)
|
2020-11-05 14:55:06 +01:00
|
|
|
other_items = total - plan.amount
|
2020-12-30 12:19:33 +01:00
|
|
|
# base monthly price of the plan
|
|
|
|
price = plan.amount
|
2020-11-05 14:55:06 +01:00
|
|
|
ps = PaymentSchedule.new(scheduled: plan, total: price + other_items, coupon: coupon)
|
2020-11-03 16:50:11 +01:00
|
|
|
deadlines = plan.duration / 1.month
|
2020-11-04 16:22:31 +01:00
|
|
|
per_month = (price / deadlines).truncate
|
2020-12-30 10:16:39 +01:00
|
|
|
adjustment = if per_month * deadlines + other_items.truncate != ps.total
|
|
|
|
ps.total - (per_month * deadlines + other_items.truncate)
|
2020-11-03 16:50:11 +01:00
|
|
|
else
|
|
|
|
0
|
|
|
|
end
|
|
|
|
items = []
|
|
|
|
(0..deadlines - 1).each do |i|
|
|
|
|
date = DateTime.current + i.months
|
2021-01-04 17:03:44 +01:00
|
|
|
details = { recurring: per_month, subscription_id: subscription&.id }
|
2020-11-05 14:55:06 +01:00
|
|
|
amount = if i.zero?
|
2020-12-30 10:16:39 +01:00
|
|
|
details[:adjustment] = adjustment.truncate
|
|
|
|
details[:other_items] = other_items.truncate
|
|
|
|
per_month + adjustment.truncate + other_items.truncate
|
2020-11-05 14:55:06 +01:00
|
|
|
else
|
|
|
|
per_month
|
|
|
|
end
|
2020-12-30 12:19:33 +01:00
|
|
|
if coupon
|
|
|
|
cs = CouponService.new
|
|
|
|
if (coupon.validity_per_user == 'once' && i.zero?) || coupon.validity_per_user == 'forever'
|
|
|
|
details[:without_coupon] = amount
|
|
|
|
amount = cs.apply(amount, coupon)
|
|
|
|
end
|
|
|
|
end
|
2020-11-03 16:50:11 +01:00
|
|
|
items.push PaymentScheduleItem.new(
|
2020-11-05 14:55:06 +01:00
|
|
|
amount: amount,
|
2020-11-03 16:50:11 +01:00
|
|
|
due_date: date,
|
2020-11-12 16:44:55 +01:00
|
|
|
payment_schedule: ps,
|
|
|
|
details: details
|
2020-11-03 16:50:11 +01:00
|
|
|
)
|
|
|
|
end
|
2021-01-20 13:47:29 +01:00
|
|
|
ps.total = items.map(&:amount).reduce(:+)
|
2020-11-03 16:50:11 +01:00
|
|
|
{ payment_schedule: ps, items: items }
|
|
|
|
end
|
2020-11-10 17:02:21 +01:00
|
|
|
|
2021-04-20 17:22:53 +02:00
|
|
|
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil,
|
|
|
|
payment_id: nil, payment_type: nil)
|
2020-12-29 18:55:00 +01:00
|
|
|
subscription = reservation.generate_subscription if !subscription && reservation&.plan_id
|
2020-12-30 10:16:39 +01:00
|
|
|
raise InvalidSubscriptionError unless subscription&.persisted?
|
2020-12-21 12:02:51 +01:00
|
|
|
|
2021-01-04 17:03:44 +01:00
|
|
|
schedule = compute(subscription.plan, total, coupon: coupon, subscription: subscription)
|
2020-11-10 17:02:21 +01:00
|
|
|
ps = schedule[:payment_schedule]
|
|
|
|
items = schedule[:items]
|
|
|
|
|
2020-12-16 18:33:43 +01:00
|
|
|
ps.scheduled = reservation || subscription
|
2020-11-10 17:02:21 +01:00
|
|
|
ps.payment_method = payment_method
|
2021-04-23 12:52:06 +02:00
|
|
|
if !payment_id.nil? && !payment_type.nil?
|
2021-04-23 17:54:59 +02:00
|
|
|
pgo = PaymentGatewayObject.new(
|
2021-04-23 12:52:06 +02:00
|
|
|
gateway_object_id: payment_id,
|
2021-04-23 17:54:59 +02:00
|
|
|
gateway_object_type: payment_type,
|
|
|
|
item: ps
|
|
|
|
)
|
|
|
|
ps.payment_gateway_objects.push(pgo)
|
2021-04-23 12:52:06 +02:00
|
|
|
end
|
2020-11-16 16:37:40 +01:00
|
|
|
ps.operator_profile = operator.invoicing_profile
|
|
|
|
ps.invoicing_profile = user.invoicing_profile
|
2021-01-25 14:37:07 +01:00
|
|
|
ps.statistic_profile = user.statistic_profile
|
2020-12-29 18:55:00 +01:00
|
|
|
ps.payment_schedule_items = items
|
2020-11-10 17:02:21 +01:00
|
|
|
items.each do |item|
|
|
|
|
item.payment_schedule = ps
|
|
|
|
end
|
2020-11-16 16:37:40 +01:00
|
|
|
ps
|
2020-11-10 17:02:21 +01:00
|
|
|
end
|
2021-01-20 17:00:23 +01:00
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
##
|
|
|
|
# Generate the invoice associated with the given PaymentScheduleItem, with the children elements (InvoiceItems).
|
2021-04-15 17:01:52 +02:00
|
|
|
# @param payment_method {String} the payment method or gateway in use
|
|
|
|
# @param payment_id {String} the identifier of the payment as provided by the payment gateway, in case of card payment
|
2021-04-21 17:38:06 +02:00
|
|
|
# @param payment_type {String} the object type of payment_id
|
2021-01-25 13:05:27 +01:00
|
|
|
##
|
2021-04-21 17:38:06 +02:00
|
|
|
def generate_invoice(payment_schedule_item, payment_method: nil, payment_id: nil, payment_type: nil)
|
2021-01-25 13:05:27 +01:00
|
|
|
# build the base invoice
|
2021-01-20 17:00:23 +01:00
|
|
|
invoice = Invoice.new(
|
|
|
|
invoiced: payment_schedule_item.payment_schedule.scheduled,
|
|
|
|
invoicing_profile: payment_schedule_item.payment_schedule.invoicing_profile,
|
2021-01-25 14:37:07 +01:00
|
|
|
statistic_profile: payment_schedule_item.payment_schedule.statistic_profile,
|
2021-01-20 17:00:23 +01:00
|
|
|
operator_profile_id: payment_schedule_item.payment_schedule.operator_profile_id,
|
2021-04-15 17:01:52 +02:00
|
|
|
payment_method: payment_method
|
2021-01-20 17:00:23 +01:00
|
|
|
)
|
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
|
2021-01-25 13:05:27 +01:00
|
|
|
# complete the invoice with some InvoiceItem
|
|
|
|
if payment_schedule_item.first?
|
|
|
|
complete_first_invoice(payment_schedule_item, invoice)
|
|
|
|
else
|
|
|
|
complete_next_invoice(payment_schedule_item, invoice)
|
|
|
|
end
|
2021-01-20 17:00:23 +01:00
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
# set the total and apply any coupon
|
|
|
|
user = payment_schedule_item.payment_schedule.user
|
|
|
|
coupon = payment_schedule_item.payment_schedule.coupon
|
|
|
|
set_total_and_coupon(payment_schedule_item, invoice, user, coupon)
|
|
|
|
|
|
|
|
# save the results
|
|
|
|
invoice.save
|
2021-01-27 17:24:10 +01:00
|
|
|
payment_schedule_item.update_attributes(invoice_id: invoice.id)
|
2021-01-25 17:42:30 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# return a paginated list of PaymentSchedule, optionally filtered, with their associated PaymentScheduleItem
|
|
|
|
# @param page {number} page number, used to paginate results
|
|
|
|
# @param size {number} number of items per page
|
|
|
|
# @param filters {Hash} allowed filters: reference, customer, date.
|
|
|
|
##
|
|
|
|
def self.list(page, size, filters = {})
|
|
|
|
ps = PaymentSchedule.includes(:invoicing_profile, :payment_schedule_items, :subscription)
|
|
|
|
.joins(:invoicing_profile)
|
2021-01-27 13:59:41 +01:00
|
|
|
.order('payment_schedules.created_at DESC')
|
2021-01-25 17:42:30 +01:00
|
|
|
.page(page)
|
|
|
|
.per(size)
|
|
|
|
|
|
|
|
|
|
|
|
unless filters[:reference].nil?
|
|
|
|
ps = ps.where(
|
|
|
|
'payment_schedules.reference LIKE :search',
|
|
|
|
search: "#{filters[:reference]}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
unless filters[:customer].nil?
|
|
|
|
# ILIKE => PostgreSQL case-insensitive LIKE
|
|
|
|
ps = ps.where(
|
|
|
|
'invoicing_profiles.first_name ILIKE :search OR invoicing_profiles.last_name ILIKE :search',
|
|
|
|
search: "%#{filters[:customer]}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
unless filters[:date].nil?
|
|
|
|
ps = ps.where(
|
2021-01-27 13:59:41 +01:00
|
|
|
"date_trunc('day', payment_schedules.created_at) = :search OR date_trunc('day', payment_schedule_items.due_date) = :search",
|
2021-01-25 17:42:30 +01:00
|
|
|
search: "%#{DateTime.iso8601(filters[:date]).to_time.to_date}%"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
ps
|
2021-01-20 17:00:23 +01:00
|
|
|
end
|
|
|
|
|
2021-02-09 15:44:56 +01:00
|
|
|
def self.cancel(payment_schedule)
|
|
|
|
# cancel all item where state != paid
|
|
|
|
payment_schedule.ordered_items.each do |item|
|
|
|
|
next if item.state == 'paid'
|
|
|
|
|
|
|
|
item.update_attributes(state: 'canceled')
|
|
|
|
end
|
|
|
|
# cancel subscription
|
|
|
|
subscription = Subscription.find(payment_schedule.payment_schedule_items.first.details['subscription_id'])
|
2021-02-09 16:26:09 +01:00
|
|
|
subscription.expire(DateTime.current)
|
2021-02-09 15:44:56 +01:00
|
|
|
|
|
|
|
subscription.canceled_at
|
|
|
|
end
|
|
|
|
|
2021-01-20 17:00:23 +01:00
|
|
|
private
|
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
##
|
|
|
|
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustement price
|
2021-02-08 15:28:47 +01:00
|
|
|
# for the subscription (if any) and the wallet transaction (if any)
|
2021-01-25 13:05:27 +01:00
|
|
|
##
|
|
|
|
def complete_first_invoice(payment_schedule_item, invoice)
|
|
|
|
# sub-prices for the subscription and the reservation
|
2021-01-25 14:37:07 +01:00
|
|
|
details = {
|
|
|
|
subscription: payment_schedule_item.details['recurring'] + payment_schedule_item.details['adjustment']
|
|
|
|
}
|
2021-01-25 13:05:27 +01:00
|
|
|
|
|
|
|
# the subscription and reservation items
|
|
|
|
subscription = Subscription.find(payment_schedule_item.details['subscription_id'])
|
|
|
|
if payment_schedule_item.payment_schedule.scheduled_type == Reservation.name
|
2021-01-25 14:37:07 +01:00
|
|
|
details[:reservation] = payment_schedule_item.details['other_items']
|
2021-01-25 13:05:27 +01:00
|
|
|
reservation = payment_schedule_item.payment_schedule.scheduled
|
2021-01-20 17:00:23 +01:00
|
|
|
end
|
|
|
|
|
2021-02-08 15:28:47 +01:00
|
|
|
# the wallet transaction
|
|
|
|
invoice[:wallet_amount] = payment_schedule_item.payment_schedule.wallet_amount
|
|
|
|
invoice[:wallet_transaction_id] = payment_schedule_item.payment_schedule.wallet_transaction_id
|
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
# build the invoice items
|
|
|
|
generate_invoice_items(invoice, details, subscription: subscription, reservation: reservation)
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# The later PaymentScheduleItems only contain references to the subscription (which is recurring)
|
|
|
|
##
|
|
|
|
def complete_next_invoice(payment_schedule_item, invoice)
|
|
|
|
# the subscription item
|
|
|
|
subscription = Subscription.find(payment_schedule_item.details['subscription_id'])
|
|
|
|
|
|
|
|
# sub-price for the subscription
|
|
|
|
details = { subscription: payment_schedule_item.details['recurring'] }
|
|
|
|
|
|
|
|
# build the invoice item
|
|
|
|
generate_invoice_items(invoice, details, subscription: subscription)
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Generate an array of InvoiceItem according to the provided parameters and saves them in invoice.invoice_items
|
|
|
|
##
|
|
|
|
def generate_invoice_items(invoice, payment_details, reservation: nil, subscription: nil)
|
|
|
|
generate_reservation_item(invoice, reservation, payment_details) if reservation
|
|
|
|
|
|
|
|
return unless subscription
|
2021-01-20 17:00:23 +01:00
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
generate_subscription_item(invoice, subscription, payment_details)
|
2021-01-20 17:00:23 +01:00
|
|
|
end
|
|
|
|
|
2021-01-25 13:05:27 +01:00
|
|
|
##
|
|
|
|
# Generate a single InvoiceItem for the given reservation and save it in invoice.invoice_items.
|
|
|
|
# This method must be called only with a valid reservation
|
|
|
|
##
|
2021-01-20 17:00:23 +01:00
|
|
|
def generate_reservation_item(invoice, reservation, payment_details)
|
|
|
|
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
|
|
|
|
|
2021-01-25 14:37:07 +01:00
|
|
|
description = "#{reservation.reservable.name}\n"
|
2021-01-20 17:00:23 +01:00
|
|
|
reservation.slots.each do |slot|
|
2021-01-25 13:05:27 +01:00
|
|
|
description += " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}\n"
|
2021-01-20 17:00:23 +01:00
|
|
|
end
|
2021-01-25 13:05:27 +01:00
|
|
|
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
|
|
|
amount: payment_details[:reservation],
|
|
|
|
description: description
|
|
|
|
)
|
2021-01-20 17:00:23 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Generate an InvoiceItem for the given subscription and save it in invoice.invoice_items.
|
|
|
|
# This method must be called only with a valid subscription
|
|
|
|
##
|
2021-01-25 13:05:27 +01:00
|
|
|
def generate_subscription_item(invoice, subscription, payment_details)
|
2021-01-20 17:00:23 +01:00
|
|
|
raise TypeError unless subscription
|
|
|
|
|
|
|
|
invoice.invoice_items.push InvoiceItem.new(
|
2021-01-25 13:05:27 +01:00
|
|
|
amount: payment_details[:subscription],
|
2021-01-20 17:00:23 +01:00
|
|
|
description: subscription.plan.name,
|
|
|
|
subscription_id: subscription.id
|
|
|
|
)
|
|
|
|
end
|
2021-01-25 13:05:27 +01:00
|
|
|
|
|
|
|
##
|
|
|
|
# Set the total price to the invoice, summing all sub-items.
|
|
|
|
# Additionally a coupon may be applied to this invoice to make a discount on the total price
|
|
|
|
##
|
|
|
|
def set_total_and_coupon(payment_schedule_item, invoice, user, coupon = nil)
|
|
|
|
return unless invoice
|
|
|
|
|
|
|
|
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
|
|
|
|
|
|
|
unless coupon.nil?
|
|
|
|
if (coupon.validity_per_user == 'once' && payment_schedule_item.first?) || coupon.validity_per_user == 'forever'
|
|
|
|
total = CouponService.new.apply(total, coupon, user.id)
|
|
|
|
invoice.coupon_id = coupon.id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
invoice.total = total
|
|
|
|
end
|
2020-11-03 16:50:11 +01:00
|
|
|
end
|