So I thought I could write this "cleaner" using a table generator.
To be fair I feel this is cleaner than the recursive CTE.
To things to note, you "max loan period possible" need to be inserted for the 1000
that I have.
The bimonthly, is done by find the number of days between the monthly options, and taking "half that" it's normal to use 15 for the odd days.
But like that:
WITH loans_table(loanid, loan_date, loan_amount,
maturity_date, payment_frequency,
payment_amount) as (
SELECT * FROM VALUES
('abc123', '2022-01-01'::date, 250, '2022-03-09'::date, 'biweekly', 50)
), table_of_numbers as (
SELECT row_number() over(order by null) as rn
FROM TABLE(generator(ROWCOUNT => 1000))
/* that 1000 should be larger than any loan perdiod length you have */
), loan_enrich as (
SELECT
*
,CASE Payment_frequency
WHEN 'weekly' THEN 7
WHEN 'biweekly' THEN 14
WHEN 'semimonthly' THEN 14
WHEN 'monthly' THEN 28
END as period_lo_days
,datediff('day', loan_date, maturity_date) as loan_days
,CEIL(loan_days / period_lo_days) as loan_periods
FROM loans_table
)
SELECT
l.loanid,
CASE payment_frequency
WHEN 'weekly' THEN dateadd('week', r.rn, l.loan_date)
WHEN 'biweekly' THEN dateadd('week', r.rn * 2, l.loan_date)
WHEN 'semimonthly' THEN
case r.rn%2
when 0 then dateadd('month', r.rn/2, l.loan_date)
when 1 then dateadd('days', floor(datediff('days', dateadd('month', (r.rn-1)/2, l.loan_date), dateadd('month', (r.rn+1)/2, l.loan_date))/2), dateadd('month', (r.rn-1)/2, l.loan_date))
end
WHEN 'monthly' THEN dateadd('month', r.rn, l.loan_date)
END as payment_date,
l.payment_amount,
l.payment_frequency
FROM loan_enrich AS l
JOIN table_of_numbers AS r
ON l.loan_periods >= r.rn
ORDER BY 1, r.rn;
gives:
LOANID |
PAYMENT_DATE |
PAYMENT_AMOUNT |
PAYMENT_FREQUENCY |
abc123 |
2022-01-15 |
50 |
biweekly |
abc123 |
2022-01-29 |
50 |
biweekly |
abc123 |
2022-02-12 |
50 |
biweekly |
abc123 |
2022-02-26 |
50 |
biweekly |
abc123 |
2022-03-12 |
50 |
biweekly |
So this can be boosted, to have semimonthly15 which is always 15 days later, and we can do some do some filtering incase the number of rows was more than we need, and we can show logic for handling a final payment that is smaller than the prior payments:
WITH loans_table(loanid, loan_date, loan_amount,
maturity_date, payment_frequency,
payment_amount) as (
SELECT * FROM VALUES
('abc123', '2022-01-01'::date, 250, '2022-03-09'::date, 'biweekly', 50),
('abc124', '2022-01-01'::date, 249, '2022-03-09'::date, 'semimonthly', 50),
('abc125', '2022-01-01'::date, 249, '2022-03-09'::date, 'semimonthly15', 50)
), table_of_numbers as (
SELECT row_number() over(order by null) as rn
FROM TABLE(generator(ROWCOUNT => 1000))
/* that 1000 should be larger than any loan perdiod length you have */
), loan_enrich as (
SELECT
*
,CASE Payment_frequency
WHEN 'weekly' THEN 7
WHEN 'biweekly' THEN 14
WHEN 'semimonthly' THEN 14
WHEN 'semimonthly15' THEN 14
WHEN 'monthly' THEN 28
END as period_lo_days
,datediff('day', loan_date, maturity_date) as loan_days
,CEIL(loan_days / period_lo_days) as loan_periods
FROM loans_table
)
SELECT
l.loanid,
CASE payment_frequency
WHEN 'weekly' THEN dateadd('week', r.rn, l.loan_date)
WHEN 'biweekly' THEN dateadd('week', r.rn * 2, l.loan_date)
WHEN 'semimonthly' THEN
case r.rn%2
when 0 then dateadd('month', r.rn/2, l.loan_date)
when 1 then dateadd('days', floor(datediff('days', dateadd('month', (r.rn-1)/2, l.loan_date), dateadd('month', (r.rn+1)/2, l.loan_date))/2), dateadd('month', (r.rn-1)/2, l.loan_date))
end
WHEN 'semimonthly15' THEN
case r.rn%2
when 0 then dateadd('month', r.rn/2, l.loan_date)
when 1 then dateadd('days', 15, dateadd('month', (r.rn-1)/2, l.loan_date))
end
WHEN 'monthly' THEN dateadd('month', r.rn, l.loan_date)
END as payment_date,
l.payment_amount,
l.payment_frequency,
l.loan_amount,
l.loan_amount - least(l.loan_amount, l.payment_amount * r.rn) as still_to_pay,
least(l.loan_amount - least(l.loan_amount, l.payment_amount * (r.rn-1)), l.payment_amount) as this_payment
FROM loan_enrich AS l
JOIN table_of_numbers AS r
ON l.loan_periods >= r.rn
WHERE this_payment > 0
ORDER BY 1, r.rn
LOANID |
PAYMENT_DATE |
PAYMENT_AMOUNT |
PAYMENT_FREQUENCY |
LOAN_AMOUNT |
STILL_TO_PAY |
THIS_PAYMENT |
abc123 |
2022-01-15 |
50 |
biweekly |
250 |
200 |
50 |
abc123 |
2022-01-29 |
50 |
biweekly |
250 |
150 |
50 |
abc123 |
2022-02-12 |
50 |
biweekly |
250 |
100 |
50 |
abc123 |
2022-02-26 |
50 |
biweekly |
250 |
50 |
50 |
abc123 |
2022-03-12 |
50 |
biweekly |
250 |
0 |
50 |
abc124 |
2022-01-16 |
50 |
semimonthly |
249 |
199 |
50 |
abc124 |
2022-02-01 |
50 |
semimonthly |
249 |
149 |
50 |
abc124 |
2022-02-15 |
50 |
semimonthly |
249 |
99 |
50 |
abc124 |
2022-03-01 |
50 |
semimonthly |
249 |
49 |
50 |
abc124 |
2022-03-16 |
50 |
semimonthly |
249 |
0 |
49 |
abc125 |
2022-01-16 |
50 |
semimonthly15 |
249 |
199 |
50 |
abc125 |
2022-02-01 |
50 |
semimonthly15 |
249 |
149 |
50 |
abc125 |
2022-02-16 |
50 |
semimonthly15 |
249 |
99 |
50 |
abc125 |
2022-03-01 |
50 |
semimonthly15 |
249 |
49 |
50 |
abc125 |
2022-03-16 |
50 |
semimonthly15 |
249 |
0 |
49 |