2020-11-03 16:50:11 +01:00
# frozen_string_literal: true
2021-06-03 12:22:37 +02:00
# perform various operations on PaymentSchedules
2020-11-03 16:50:11 +01:00
class PaymentScheduleService
# Compute a payment schedule for a new subscription to the provided plan
2023-02-17 11:44:04 +01:00
# @param plan [Plan]
# @param total [Number] Total amount of the current shopping cart (which includes this plan) - without coupon
# @param customer [User] the customer
# @param coupon [Coupon] apply this coupon, if any
# @param start_at [Time] schedule the PaymentSchedule to start in the future
2021-10-14 18:20:10 +02:00
def compute ( plan , total , customer , coupon : nil , start_at : 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
2021-05-27 16:11:23 +02:00
ps = PaymentSchedule . new ( 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
2022-10-25 10:52:33 +02:00
adjustment = if ( per_month * deadlines ) + other_items . truncate == ps . total
2020-11-03 16:50:11 +01:00
0
2022-10-25 10:52:33 +02:00
else
ps . total - ( ( per_month * deadlines ) + other_items . truncate )
2020-11-03 16:50:11 +01:00
end
items = [ ]
( 0 .. deadlines - 1 ) . each do | i |
2022-10-25 10:52:33 +02:00
items . push compute_deadline ( i , ps , per_month , adjustment , other_items , coupon : coupon , schedule_start_at : start_at )
2020-11-03 16:50:11 +01:00
end
2021-10-18 15:19:58 +02:00
ps . start_at = start_at
2021-01-20 13:47:29 +01:00
ps . total = items . map ( & :amount ) . reduce ( :+ )
2021-10-14 18:20:10 +02:00
ps . invoicing_profile = customer . invoicing_profile
ps . statistic_profile = customer . statistic_profile
2020-11-03 16:50:11 +01:00
{ payment_schedule : ps , items : items }
end
2020-11-10 17:02:21 +01:00
2022-10-25 10:52:33 +02:00
def compute_deadline ( deadline_index , payment_schedule , price_per_month , adjustment_price , other_items_price ,
coupon : nil , schedule_start_at : nil )
2023-02-14 13:10:58 +01:00
date = ( schedule_start_at || Time . current ) + deadline_index . months
2022-10-25 10:52:33 +02:00
details = { recurring : price_per_month }
amount = if deadline_index . zero?
details [ :adjustment ] = adjustment_price . truncate
details [ :other_items ] = other_items_price . truncate
price_per_month + adjustment_price . truncate + other_items_price . truncate
else
price_per_month
end
if coupon
cs = CouponService . new
if ( coupon . validity_per_user == 'once' && deadline_index . zero? ) || coupon . validity_per_user == 'forever'
details [ :without_coupon ] = amount
amount = cs . apply ( amount , coupon )
end
end
PaymentScheduleItem . new (
amount : amount ,
due_date : date ,
payment_schedule : payment_schedule ,
details : details
)
end
2021-10-14 18:20:10 +02:00
def create ( objects , total , customer , coupon : nil , operator : nil , payment_method : nil ,
2021-04-20 17:22:53 +02:00
payment_id : nil , payment_type : nil )
2022-10-25 10:52:33 +02:00
subscription = objects . find { | item | item . instance_of? ( Subscription ) }
2020-12-21 12:02:51 +01:00
2021-10-14 18:20:10 +02:00
schedule = compute ( subscription . plan , total , customer , coupon : coupon , start_at : subscription . start_at )
2020-11-10 17:02:21 +01:00
ps = schedule [ :payment_schedule ]
items = schedule [ :items ]
2021-05-28 17:34:20 +02:00
ps . payment_schedule_objects = build_objects ( objects )
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
2020-12-29 18:55:00 +01:00
ps . payment_schedule_items = items
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-05-28 17:34:20 +02:00
def build_objects ( objects )
res = [ ]
res . push ( PaymentScheduleObject . new ( object : objects [ 0 ] , main : true ) )
2022-10-25 10:52:33 +02:00
objects [ 1 .. ] . each do | object |
2021-05-28 17:34:20 +02:00
res . push ( PaymentScheduleObject . new ( object : object ) )
end
res
end
2021-01-25 13:05:27 +01:00
# Generate the invoice associated with the given PaymentScheduleItem, with the children elements (InvoiceItems).
2023-02-17 11:44:04 +01: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
# @param payment_type [String] the object type of payment_id
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 (
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
2023-03-27 11:43:36 +02:00
invoice . payment_schedule_item = payment_schedule_item
2021-01-25 13:05:27 +01:00
invoice . save
2021-01-25 17:42:30 +01:00
end
# return a paginated list of PaymentSchedule, optionally filtered, with their associated PaymentScheduleItem
2023-02-17 11:44:04 +01:00
# @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.
2021-01-25 17:42:30 +01:00
def self . list ( page , size , filters = { } )
2022-01-11 16:04:14 +01:00
ps = PaymentSchedule . includes ( :operator_profile , :payment_schedule_items , invoicing_profile : [ :user ] )
2021-01-25 17:42:30 +01:00
. 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?
2023-11-23 11:29:08 +01:00
start_at = Date . parse ( filters [ :date ] ) . in_time_zone
end_at = start_at . end_of_day
ps = ps . where ( " (payment_schedules.created_at BETWEEN :start_at AND :end_at) OR (payment_schedule_items.due_date BETWEEN :start_at AND :end_at) " , start_at : start_at , end_at : end_at ) . references ( :payment_schedule_items )
2021-01-25 17:42:30 +01:00
end
ps
2021-01-20 17:00:23 +01:00
end
2022-01-17 12:38:53 +01:00
##
# Cancel the given PaymentSchedule: cancel the remote subscription on the payment gateway, mark the PaymentSchedule as cancelled,
# the remaining PaymentScheduleItems as canceled too, and cancel the associated Subscription.
##
2021-02-09 15:44:56 +01:00
def self . cancel ( payment_schedule )
2022-01-03 17:13:35 +01:00
PaymentGatewayService . new . cancel_subscription ( payment_schedule )
2021-02-09 15:44:56 +01:00
# cancel all item where state != paid
payment_schedule . ordered_items . each do | item |
next if item . state == 'paid'
2022-10-25 10:52:33 +02:00
item . update ( state : 'canceled' )
2021-02-09 15:44:56 +01:00
end
# cancel subscription
2021-06-24 16:52:47 +02:00
subscription = payment_schedule . payment_schedule_objects . find { | pso | pso . object_type == Subscription . name } . subscription
2023-02-17 15:35:06 +01:00
subscription . expire
2021-02-09 15:44:56 +01:00
subscription . canceled_at
end
2022-01-17 12:38:53 +01:00
##
# Update the payment mean associated with the given PaymentSchedule and reset the erroneous items
##
2024-02-08 15:59:17 +01:00
def update_payment_mean ( payment_schedule_item , payment_method )
payment_schedule_item . update ( payment_method : payment_method , state : payment_schedule_item . due_date < Time . current ? 'pending' : 'new' )
2022-01-17 12:38:53 +01:00
end
2021-01-20 17:00:23 +01:00
private
2022-01-17 12:38:53 +01:00
##
# After the payment method has been updated, we need to reset the erroneous payment schedule items
# so the admin can confirm them to generate the invoice
##
def reset_erroneous_payment_schedule_items ( payment_schedule )
results = payment_schedule . payment_schedule_items . where ( state : %w[ error gateway_canceled ] ) . map do | item |
2023-02-14 13:10:58 +01:00
item . update ( state : item . due_date < Time . current ? 'pending' : 'new' )
2022-01-17 12:38:53 +01:00
end
results . reduce ( true ) { | acc , item | acc && item }
end
2021-01-25 13:05:27 +01:00
##
2021-05-27 15:58:55 +02:00
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustment 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
2022-10-25 10:52:33 +02:00
subscription = payment_schedule_item . payment_schedule
. payment_schedule_objects
. find { | pso | pso . object_type == Subscription . name }
. subscription
2021-05-27 15:58:55 +02:00
if payment_schedule_item . payment_schedule . main_object . object_type == Reservation . name
2021-01-25 14:37:07 +01:00
details [ :reservation ] = payment_schedule_item . details [ 'other_items' ]
2021-06-03 12:22:37 +02:00
reservation = payment_schedule_item . payment_schedule . main_object . reservation
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
2022-10-25 10:52:33 +02:00
subscription = payment_schedule_item . payment_schedule
. payment_schedule_objects
. find { | pso | pso . object_type == Subscription . name }
. subscription
2021-01-25 13:05:27 +01:00
# 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
2022-10-25 10:52:33 +02:00
generate_subscription_item ( invoice , subscription , payment_details , main : reservation . nil? )
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 ] ,
2021-05-27 15:58:55 +02:00
description : description ,
object : reservation ,
main : true
2021-01-25 13:05:27 +01:00
)
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
##
2022-10-25 10:52:33 +02:00
def generate_subscription_item ( invoice , subscription , payment_details , main : true )
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-09-14 16:35:18 +02:00
description : subscription . plan . base_name ,
2021-05-27 15:58:55 +02:00
object : subscription ,
main : main
2021-01-20 17:00:23 +01:00
)
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 ( :+ )
2022-10-25 10:52:33 +02:00
if ! coupon . nil? && ( ( 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
2021-01-25 13:05:27 +01:00
end
invoice . total = total
end
2020-11-03 16:50:11 +01:00
end