FINERACT-1981: Progressive Loan schedule handling - Reschedule installment (Extend repayment period)

This commit is contained in:
Soma Sörös
2025-08-21 10:22:07 +02:00
committed by Adam Saghy
parent 47b55b5ee6
commit 14589e2600
10 changed files with 752 additions and 95 deletions
@@ -5809,8 +5809,7 @@ Feature: Loan
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 February 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
# TODO unskip and check when PS-1654 is done
@Skip @TestRailId:C3127
@TestRailId:C3127
Scenario: Verify fixed length loan account Loan schedule - UC9: Reschedule by extra terms a loan account with fixed length of 40 days
When Admin sets the business date to "01 February 2024"
When Admin creates a client with random data
@@ -5843,9 +5842,9 @@ Feature: Loan
| 1 | 0 | 01 February 2024 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 15 | 16 February 2024 | | 600.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 3 | 15 | 02 March 2024 | | 450.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 4 | 15 | 17 March 2024 | | 300.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 4 | 10 | 12 March 2024 | | 300.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 5 | 15 | 01 April 2024 | | 150.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 6 | 10 | 11 April 2024 | | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
| 6 | 15 | 16 April 2024 | | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 | 0.0 | 0.0 | 0.0 | 150.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0 | 0.0 | 0 | 1000.0 | 0.0 | 0.0 | 0 | 1000.0 |
@@ -7809,20 +7808,20 @@ Feature: Loan
Then Loan's all installments have obligations met
@TestRailId:C3963
Scenario: Verify Progressive Loan reschedule by extending repayment period: Basic scenario without downpayment
Scenario: Verify Progressive Loan reschedule by extending repayment period: Basic scenario without downpayment, flat interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1000" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 666.67 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 |
| 2 | 31 | 01 August 2024 | | 333.34 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 | 0.0 | 0.0 | 0.0 | 333.33 |
| 3 | 31 | 01 September 2024 | | 0.0 | 333.34 | 0.0 | 0.0 | 0.0 | 333.34 | 0.0 | 0.0 | 0.0 | 333.34 |
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 667.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 |
| 2 | 31 | 01 August 2024 | | 334.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 |
| 3 | 31 | 01 September 2024 | | 0.0 | 334.0 | 0.0 | 0.0 | 0.0 | 334.0 | 0.0 | 0.0 | 0.0 | 334.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@@ -7836,10 +7835,10 @@ Feature: Loan
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 31 | 01 August 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 3 | 31 | 01 September 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 30 | 01 October 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 1 | 30 | 01 July 2024 | | 667.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 | 0.0 | 0.0 | 0.0 | 333.0 |
| 2 | 31 | 01 August 2024 | | 445.0 | 222.0 | 0.0 | 0.0 | 0.0 | 222.0 | 0.0 | 0.0 | 0.0 | 222.0 |
| 3 | 31 | 01 September 2024 | | 223.0 | 222.0 | 0.0 | 0.0 | 0.0 | 222.0 | 0.0 | 0.0 | 0.0 | 222.0 |
| 4 | 30 | 01 October 2024 | | 0.0 | 223.0 | 0.0 | 0.0 | 0.0 | 223.0 | 0.0 | 0.0 | 0.0 | 223.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@@ -7848,7 +7847,7 @@ Feature: Loan
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@TestRailId:C3964
Scenario: Verify Progressive Loan reschedule by extending repayment period with downpayment installment
Scenario: Verify Progressive Loan reschedule by extending repayment period with downpayment installment, flat interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
@@ -7872,7 +7871,7 @@ Feature: Loan
When Admin sets the business date to "01 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 August 2024 | 01 July 2024 | | | | 1 | |
| 01 July 2024 | 01 July 2024 | | | | 1 | |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -7889,12 +7888,12 @@ Feature: Loan
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@TestRailId:C3965
Scenario: Verify Progressive Loan reschedule by extending repayment period - multiple extra terms
Scenario: Verify Progressive Loan reschedule by extending repayment period - multiple extra terms, flat interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1500 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1500 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1500" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1500" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
@@ -7912,7 +7911,7 @@ Feature: Loan
When Admin sets the business date to "01 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 August 2024 | 01 July 2024 | | | | 2 | |
| 01 July 2024 | 01 July 2024 | | | | 2 | |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -7929,12 +7928,12 @@ Feature: Loan
| 01 June 2024 | Disbursement | 1500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1500.0 |
@TestRailId:C3966
Scenario: Verify Progressive Loan reschedule by extending repayment period after partial repayment
Scenario: Verify Progressive Loan reschedule by extending repayment period after partial repayment, flat interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1200 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1200 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1200" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1200" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
@@ -7969,12 +7968,12 @@ Feature: Loan
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 August 2024 | 15 July 2024 | | | | 1 | |
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 533.33 | 266.67 | 0.0 | 0.0 | 0.0 | 266.67 | 0.0 | 0.0 | 0.0 | 266.67 |
| 3 | 31 | 01 September 2024 | | 266.66 | 266.67 | 0.0 | 0.0 | 0.0 | 266.67 | 0.0 | 0.0 | 0.0 | 266.67 |
| 4 | 30 | 01 October 2024 | | 0.0 | 266.66 | 0.0 | 0.0 | 0.0 | 266.66 | 0.0 | 0.0 | 0.0 | 266.66 |
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 533.0 | 267.0 | 0.0 | 0.0 | 0.0 | 267.0 | 0.0 | 0.0 | 0.0 | 267.0 |
| 3 | 31 | 01 September 2024 | | 266.0 | 267.0 | 0.0 | 0.0 | 0.0 | 267.0 | 0.0 | 0.0 | 0.0 | 267.0 |
| 4 | 30 | 01 October 2024 | | 0.0 | 266.0 | 0.0 | 0.0 | 0.0 | 266.0 | 0.0 | 0.0 | 0.0 | 266.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 | 400.0 | 0.0 | 0.0 | 800.0 |
@@ -7982,3 +7981,254 @@ Feature: Loan
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 800.0 |
@TestRailId:C3967
Scenario: Verify Progressive Loan reschedule by extending repayment periods after partial repayment and then backdated repayment occurs, flat interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin set "LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1200 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1200" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1200" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
| 2 | 31 | 01 August 2024 | | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
| 3 | 31 | 01 September 2024 | | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
When Admin sets the business date to "01 July 2024"
And Customer makes "AUTOPAY" repayment on "01 July 2024" with 400 EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
| 3 | 31 | 01 September 2024 | | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 | 400.0 | 0.0 | 0.0 | 800.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 800.0 |
When Admin sets the business date to "15 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 September 2024 | 15 July 2024 | | | | 2 | |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 |
| 3 | 31 | 01 September 2024 | | 267.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 |
| 4 | 30 | 01 October 2024 | | 134.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 |
| 5 | 31 | 01 November 2024 | | 0.0 | 134.0 | 0.0 | 0.0 | 0.0 | 134.0 | 0.0 | 0.0 | 0.0 | 134.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 | 400.0 | 0.0 | 0.0 | 800.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 800.0 |
When Admin sets the business date to "15 October 2024"
And Customer makes "AUTOPAY" repayment on "01 August 2024" with 500 EUR transaction amount
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 800.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | 01 August 2024 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 September 2024 | | 267.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 |
| 4 | 30 | 01 October 2024 | | 134.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 | 0.0 | 0.0 | 0.0 | 133.0 |
| 5 | 31 | 01 November 2024 | | 0.0 | 134.0 | 0.0 | 0.0 | 0.0 | 134.0 | 100.0 | 100.0 | 0.0 | 34.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 0.0 | 0.0 | 0.0 | 1200.0 | 900.0 | 100.0 | 0.0 | 300.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 400.0 | 400.0 | 0.0 | 0.0 | 0.0 | 800.0 |
| 01 August 2024 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | 0.0 | 300.0 |
When Admin set "LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
@TestRailId:C3987
Scenario: Verify Progressive Loan reschedule by extending repayment period: Basic scenario without downpayment, declining balance interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1000" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 669.98 | 330.02 | 10.0 | 0.0 | 0.0 | 340.02 | 0.0 | 0.0 | 0.0 | 340.02 |
| 2 | 31 | 01 August 2024 | | 336.66 | 333.32 | 6.7 | 0.0 | 0.0 | 340.02 | 0.0 | 0.0 | 0.0 | 340.02 |
| 3 | 31 | 01 September 2024 | | 0.0 | 336.66 | 3.37 | 0.0 | 0.0 | 340.03 | 0.0 | 0.0 | 0.0 | 340.03 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 20.07 | 0.0 | 0.0 | 1020.07 | 0.0 | 0.0 | 0.0 | 1020.07 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
When Admin sets the business date to "01 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 August 2024 | 01 July 2024 | | | | 1 | |
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 669.98 | 330.02 | 10.0 | 0.0 | 0.0 | 340.02 | 0.0 | 0.0 | 0.0 | 340.02 |
| 2 | 31 | 01 August 2024 | | 448.87 | 221.11 | 6.7 | 0.0 | 0.0 | 227.81 | 0.0 | 0.0 | 0.0 | 227.81 |
| 3 | 31 | 01 September 2024 | | 225.55 | 223.32 | 4.49 | 0.0 | 0.0 | 227.81 | 0.0 | 0.0 | 0.0 | 227.81 |
| 4 | 30 | 01 October 2024 | | 0.0 | 225.55 | 2.26 | 0.0 | 0.0 | 227.81 | 0.0 | 0.0 | 0.0 | 227.81 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 23.45 | 0.0 | 0.0 | 1023.45 | 0.0 | 0.0 | 0.0 | 1023.45 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@TestRailId:C3989
Scenario: Verify Progressive Loan reschedule by extending repayment period with downpayment installment, declining balance interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_DOWNPAYMENT_INTEREST | 01 June 2024 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
And Admin successfully approves the loan on "01 June 2024" with "1000" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 June 2024 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 30 | 01 July 2024 | | 502.4 | 247.6 | 7.4 | 0.0 | 0.0 | 255.0 | 0.0 | 0.0 | 0.0 | 255.0 |
| 3 | 31 | 01 August 2024 | | 252.52 | 249.88 | 5.12 | 0.0 | 0.0 | 255.0 | 0.0 | 0.0 | 0.0 | 255.0 |
| 4 | 31 | 01 September 2024 | | 0.0 | 252.52 | 2.57 | 0.0 | 0.0 | 255.09 | 0.0 | 0.0 | 0.0 | 255.09 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 15.09 | 0.0 | 0.0 | 1015.09 | 0.0 | 0.0 | 0.0 | 1015.09 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
When Admin sets the business date to "01 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 July 2024 | 01 July 2024 | | | | 1 | |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 June 2024 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 30 | 01 July 2024 | | 629.4 | 120.6 | 7.4 | 0.0 | 0.0 | 128.0 | 0.0 | 0.0 | 0.0 | 128.0 |
| 3 | 31 | 01 August 2024 | | 507.81 | 121.59 | 6.41 | 0.0 | 0.0 | 128.0 | 0.0 | 0.0 | 0.0 | 128.0 |
| 4 | 31 | 01 September 2024 | | 384.99 | 122.82 | 5.18 | 0.0 | 0.0 | 128.0 | 0.0 | 0.0 | 0.0 | 128.0 |
| 5 | 30 | 01 October 2024 | | 0.0 | 384.99 | 3.8 | 0.0 | 0.0 | 388.79 | 0.0 | 0.0 | 0.0 | 388.79 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 22.79 | 0.0 | 0.0 | 1022.79 | 0.0 | 0.0 | 0.0 | 1022.79 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
@TestRailId:C3990
Scenario: Verify Progressive Loan reschedule by extending repayment period - multiple extra terms, declining balance interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1500 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1500" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1500" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 1004.97 | 495.03 | 15.0 | 0.0 | 0.0 | 510.03 | 0.0 | 0.0 | 0.0 | 510.03 |
| 2 | 31 | 01 August 2024 | | 504.99 | 499.98 | 10.05 | 0.0 | 0.0 | 510.03 | 0.0 | 0.0 | 0.0 | 510.03 |
| 3 | 31 | 01 September 2024 | | 0.0 | 504.99 | 5.05 | 0.0 | 0.0 | 510.04 | 0.0 | 0.0 | 0.0 | 510.04 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1500.0 | 30.1 | 0.0 | 0.0 | 1530.1 | 0.0 | 0.0 | 0.0 | 1530.1 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1500.0 |
When Admin sets the business date to "01 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 July 2024 | 01 July 2024 | | | | 2 | |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 1205.94 | 294.06 | 15.0 | 0.0 | 0.0 | 309.06 | 0.0 | 0.0 | 0.0 | 309.06 |
| 2 | 31 | 01 August 2024 | | 908.94 | 297.0 | 12.06 | 0.0 | 0.0 | 309.06 | 0.0 | 0.0 | 0.0 | 309.06 |
| 3 | 31 | 01 September 2024 | | 608.97 | 299.97 | 9.09 | 0.0 | 0.0 | 309.06 | 0.0 | 0.0 | 0.0 | 309.06 |
| 4 | 30 | 01 October 2024 | | 306.0 | 302.97 | 6.09 | 0.0 | 0.0 | 309.06 | 0.0 | 0.0 | 0.0 | 309.06 |
| 5 | 31 | 01 November 2024 | | 0.0 | 306.0 | 3.06 | 0.0 | 0.0 | 309.06 | 0.0 | 0.0 | 0.0 | 309.06 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1500.0 | 45.3 | 0.0 | 0.0 | 1545.3 | 0.0 | 0.0 | 0.0 | 1545.3 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1500.0 |
@TestRailId:C3994
Scenario: Verify Progressive Loan reschedule by extending repayment period after partial repayment, declining balance interest type
When Admin sets the business date to "01 June 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 June 2024 | 1200 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 June 2024" with "1200" amount and expected disbursement date on "01 June 2024"
When Admin successfully disburse the loan on "01 June 2024" with "1200" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | | 803.97 | 396.03 | 12.0 | 0.0 | 0.0 | 408.03 | 0.0 | 0.0 | 0.0 | 408.03 |
| 2 | 31 | 01 August 2024 | | 403.98 | 399.99 | 8.04 | 0.0 | 0.0 | 408.03 | 0.0 | 0.0 | 0.0 | 408.03 |
| 3 | 31 | 01 September 2024 | | 0.0 | 403.98 | 4.04 | 0.0 | 0.0 | 408.02 | 0.0 | 0.0 | 0.0 | 408.02 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 24.08 | 0.0 | 0.0 | 1224.08 | 0.0 | 0.0 | 0.0 | 1224.08 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
When Admin sets the business date to "01 July 2024"
And Customer makes "AUTOPAY" repayment on "01 July 2024" with 417.03 EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 803.97 | 396.03 | 12.0 | 0.0 | 0.0 | 408.03 | 408.03 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 403.89 | 400.08 | 7.95 | 0.0 | 0.0 | 408.03 | 9.0 | 9.0 | 0.0 | 399.03 |
| 3 | 31 | 01 September 2024 | | 0.0 | 403.89 | 4.04 | 0.0 | 0.0 | 407.93 | 0.0 | 0.0 | 0.0 | 407.93 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 23.99 | 0.0 | 0.0 | 1223.99 | 417.03 | 9.0 | 0.0 | 806.96 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 417.03 | 405.03 | 12.0 | 0.0 | 0.0 | 794.97 |
When Admin sets the business date to "15 July 2024"
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 August 2024 | 15 July 2024 | | | | 1 | |
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 June 2024 | | 1200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 July 2024 | 01 July 2024 | 803.97 | 396.03 | 12.0 | 0.0 | 0.0 | 408.03 | 408.03 | 0.0 | 0.0 | 0.0 |
| 2 | 31 | 01 August 2024 | | 538.58 | 265.39 | 7.95 | 0.0 | 0.0 | 273.34 | 9.0 | 9.0 | 0.0 | 264.34 |
| 3 | 31 | 01 September 2024 | | 270.63 | 267.95 | 5.39 | 0.0 | 0.0 | 273.34 | 0.0 | 0.0 | 0.0 | 273.34 |
| 4 | 30 | 01 October 2024 | | 0.0 | 270.63 | 2.71 | 0.0 | 0.0 | 273.34 | 0.0 | 0.0 | 0.0 | 273.34 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1200.0 | 28.05 | 0.0 | 0.0 | 1228.05 | 417.03 | 9.0 | 0.0 | 811.02 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 417.03 | 405.03 | 12.0 | 0.0 | 0.0 | 794.97 |
@@ -53,6 +53,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@@ -234,9 +235,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
for (final ChangeOperation changeOperation : changeOperations) {
if (changeOperation.isInterestRateChange()) {
final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get();
processInterestRateChange(installments, interestRateChange, scheduleModel);
if (changeOperation.isLoanTermVariationsData()) {
final LoanTermVariationsData interestRateChange = changeOperation.getLoanTermVariationsData().get();
processLoanTermVariation(installments, interestRateChange, scheduleModel);
} else if (changeOperation.isTransaction()) {
LoanTransaction transaction = changeOperation.getLoanTransaction().get();
processSingleTransaction(transaction, ctx);
@@ -301,16 +302,55 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.map(TransactionChangeData::getNewTransaction).findFirst().orElse(transaction);
}
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateChange.getTermVariationType().isInterestPauseVariation()) {
final LocalDate pauseEndDate = interestRateChange.getDateValue();
emiCalculator.applyInterestPause(scheduleModel, interestRateChangeSubmittedOnDate, pauseEndDate);
} else {
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
private void processLoanTermVariation(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
switch (termVariationsData.getTermVariationType()) {
case INTEREST_PAUSE -> handleInterestPause(installments, termVariationsData, scheduleModel);
case INTEREST_RATE_FROM_INSTALLMENT -> handleChangeInterestRate(installments, termVariationsData, scheduleModel);
case EXTEND_REPAYMENT_PERIOD -> handleExtraRepaymentPeriod(installments, termVariationsData, scheduleModel);
default -> throw new IllegalStateException("Unhandled LoanTermVariationType.");
}
}
private void handleExtraRepaymentPeriod(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final int repaymentPeriodsToAdd = termVariationsData.getDecimalValue().intValue();
emiCalculator.addRepaymentPeriods(scheduleModel, interestRateChangeSubmittedOnDate, repaymentPeriodsToAdd);
final Loan loan = installments.getFirst().getLoan();
int nextInstallmentNumber = installments.stream().mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber).max().orElse(0)
+ 1;
for (int i = 0; i < scheduleModel.repaymentPeriods().size(); i++) {
final RepaymentPeriod rp = scheduleModel.repaymentPeriods().get(i);
// Check if this period already exists in installments
if (installments.stream().noneMatch(installment -> installment.getFromDate().equals(rp.getFromDate())
&& installment.getDueDate().equals(rp.getDueDate()))) {
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(loan, nextInstallmentNumber,
rp.getFromDate(), rp.getDueDate(), rp.getDuePrincipal().getAmount(), rp.getDueInterest().getAmount(), ZERO, ZERO,
false, null, ZERO);
installments.add(newInstallment);
nextInstallmentNumber++;
}
}
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void handleChangeInterestRate(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = termVariationsData.getDecimalValue();
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void handleInterestPause(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final LocalDate pauseEndDate = termVariationsData.getDateValue();
emiCalculator.applyInterestPause(scheduleModel, interestRateChangeSubmittedOnDate, pauseEndDate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
@@ -1204,14 +1244,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<ChangeOperation> changeOperations = new ArrayList<>();
Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariationsMap = loanTermVariations.stream()
.collect(Collectors.groupingBy(ltvd -> LoanTermVariationType.fromInt(ltvd.getTermType().getId().intValue())));
if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT) != null) {
changeOperations.addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT).stream()
.map(ChangeOperation::new).toList());
}
if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE) != null) {
changeOperations
.addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE).stream().map(ChangeOperation::new).toList());
}
Stream.of(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT, LoanTermVariationType.INTEREST_PAUSE,
LoanTermVariationType.EXTEND_REPAYMENT_PERIOD).forEach(key -> {
if (loanTermVariationsMap.get(key) != null) {
changeOperations.addAll(loanTermVariationsMap.get(key).stream().map(ChangeOperation::new).toList());
}
});
if (charges != null) {
changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList());
}
@@ -32,30 +32,30 @@ import org.springframework.lang.NonNull;
@Getter
public class ChangeOperation implements Comparable<ChangeOperation> {
private final Optional<LoanTermVariationsData> interestRateChange;
private final Optional<LoanTermVariationsData> loanTermVariationsData;
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChangeOperation(LoanCharge loanCharge) {
this.interestRateChange = Optional.empty();
this.loanTermVariationsData = Optional.empty();
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChangeOperation(LoanTransaction loanTransaction) {
this.interestRateChange = Optional.empty();
this.loanTermVariationsData = Optional.empty();
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
public ChangeOperation(LoanTermVariationsData interestRateChange) {
this.interestRateChange = Optional.of(interestRateChange);
public ChangeOperation(LoanTermVariationsData loanTermVariationsData) {
this.loanTermVariationsData = Optional.of(loanTermVariationsData);
this.loanTransaction = Optional.empty();
this.loanCharge = Optional.empty();
}
public boolean isInterestRateChange() {
return interestRateChange.isPresent();
public boolean isLoanTermVariationsData() {
return loanTermVariationsData.isPresent();
}
public boolean isTransaction() {
@@ -80,7 +80,7 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private LocalDate getEffectiveDate() {
if (interestRateChange.isPresent()) {
if (loanTermVariationsData.isPresent()) {
return getSubmittedOnDate();
} else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
@@ -98,8 +98,8 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private LocalDate getSubmittedOnDate() {
if (interestRateChange.isPresent()) {
return interestRateChange.get().getTermVariationApplicableFrom();
if (loanTermVariationsData.isPresent()) {
return loanTermVariationsData.get().getTermVariationApplicableFrom();
} else if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
@@ -110,7 +110,7 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private OffsetDateTime getCreatedDateTime() {
if (interestRateChange.isPresent()) {
if (loanTermVariationsData.isPresent()) {
return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate());
} else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
@@ -38,6 +38,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -78,42 +79,73 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
BigDecimal interestRate = validateInterestRateParam(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
Integer extraTerms = validateExtraTermsParam(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
boolean hasInterestRateChange = interestRate != null;
boolean hasAdjustDueDateChange = adjustedDueDate != null;
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
} else {
validateLoanIsActive(loan, dataValidatorBuilder);
}
boolean hasExtraTermsChange = extraTerms != null;
if (hasInterestRateChange && hasAdjustDueDateChange) {
if (Stream.of(hasInterestRateChange, hasAdjustDueDateChange, hasExtraTermsChange).filter(f -> f).count() > 1) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.adjustedDueDateParamName).failWithCode(
RescheduleLoansApiConstants.rescheduleMultipleOperationsNotSupportedErrorCode,
"Only one operation is supported at a time during Loan Rescheduling");
}
if (rescheduleFromDate != null && hasInterestRateChange) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
if (hasExtraTermsChange) {
validateExtraTerms(dataValidatorBuilder, loan);
} else if (hasAdjustDueDateChange) {
validateAdjustDueDateChange(dataValidatorBuilder, loan, rescheduleFromDate);
} else if (hasInterestRateChange) {
validateInterestRate(dataValidatorBuilder, loan, rescheduleFromDate);
}
LoanRepaymentScheduleInstallment installment;
if (hasInterestRateChange) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
} else {
installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private void validateAdjustDueDateChange(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) {
validateLoanIsActive(loan, dataValidatorBuilder);
LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private void validateInterestRate(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
if (rescheduleFromDate != null) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
}
LoanRepaymentScheduleInstallment installment;
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private void validateExtraTerms(DataValidatorBuilder dataValidatorBuilder, Loan loan) {
validateLoanIsActive(loan, dataValidatorBuilder);
}
private Integer validateExtraTermsParam(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder, Loan loan) {
final Integer extraTerms = fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.extraTermsParamName,
jsonElement);
DataValidatorBuilder extraTermsDataValidator = dataValidatorBuilder.reset()
.parameter(RescheduleLoansApiConstants.extraTermsParamName).value(extraTerms).ignoreIfNull().integerGreaterThanZero();
if (extraTerms != null) {
Integer maxNumberOfRepayments = loan.getLoanProduct().getMaxNumberOfRepayments();
if (maxNumberOfRepayments != null) {
Integer numberOfRepayments = loan.getLoanProductRelatedDetail().getNumberOfRepayments();
extraTermsDataValidator.notGreaterThanMax(maxNumberOfRepayments - numberOfRepayments);
}
}
return extraTerms;
}
@Override
public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder, LoanRepaymentScheduleInstallment installment) {
if (installment == null) {
@@ -125,7 +157,6 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
private void validateUnsupportedParams(JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
final var unsupportedFields = List.of(RescheduleLoansApiConstants.graceOnPrincipalParamName, //
RescheduleLoansApiConstants.graceOnInterestParamName, //
RescheduleLoansApiConstants.extraTermsParamName, //
RescheduleLoansApiConstants.emiParamName//
);
@@ -152,6 +183,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;
boolean hasExtraTerms = false;
boolean hasInterestRateChange = false;
for (LoanRescheduleRequestToTermVariationMapping mapping : loanRescheduleRequest
.getLoanRescheduleRequestToTermVariationMappings()) {
@@ -159,6 +191,9 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
if (termType.isInterestRateVariation() || termType.isInterestRateFromInstallment()) {
hasInterestRateChange = true;
}
if (termType.isExtendRepaymentPeriod()) {
hasExtraTerms = true;
}
}
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
@@ -166,13 +201,15 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateLoanIsActive(loan, dataValidatorBuilder);
}
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null) {
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null || hasExtraTerms) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
} else {
installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!hasExtraTerms) {
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
@@ -198,7 +235,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
}
}
private BigDecimal validateInterestRate(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
private BigDecimal validateInterestRateParam(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder, Loan loan) {
final BigDecimal interestRate = fromJsonHelper
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
@@ -78,6 +78,9 @@ public interface EMICalculator {
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestSubmittedOnDate,
BigDecimal newInterestRate);
void addRepaymentPeriods(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate submittedOnDate,
int numberOfRepaymentPeriodsToAdd);
/**
* This method applies outstanding balance correction on the interest model. Negative amount decreases the
* outstanding balance while positive amounts are increasing that. Typically used for late repayment or to count
@@ -26,13 +26,16 @@ import java.time.LocalDate;
import java.time.Year;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -178,6 +181,58 @@ public final class ProgressiveEMICalculator implements EMICalculator {
changeInterestRate(scheduleModel, EmiChangeOperation.changeInterestRate(newInterestSubmittedOnDate, newInterestRate));
}
@Override
public void addRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate submittedOnDate,
final int numberOfRepaymentPeriodsToAdd) {
addRepaymentPeriods(scheduleModel,
EmiChangeOperation.addRepaymentPeriods(submittedOnDate, scheduleModel.zero(), numberOfRepaymentPeriodsToAdd));
}
public void addRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
LocalDate disbursementDate = scheduleModel.getStartDate();
int repaymentPeriodCount = scheduleModel.repaymentPeriods().size();
final LocalDate interestRateChangeEffectiveDate = operation.getSubmittedOnDate().minusDays(1);
List<LocalDateInterval> periods2 = generateAdditionalRepaymentPeriodDueDates(scheduleModel, operation.getPeriodsToAdd(),
repaymentPeriodCount, scheduleModel.resolveRepaymentPEriodLengthGeneratorFunction(disbursementDate));
updateModel(scheduleModel, periods2, LocalDateInterval::startDate, LocalDateInterval::endDate);
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
scheduleModel.zero(), scheduleModel.zero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
operation));
}
public <T> void updateModel(ProgressiveLoanInterestScheduleModel scheduleModel, List<T> updateExpectedRepaymentPeriods,
Function<T, LocalDate> from, Function<T, LocalDate> to) {
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
MathContext mc = scheduleModel.mc();
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
RepaymentPeriod originalLAstRepaymentPeriod = scheduleModel.getLastRepaymentPeriod();
prev.set(originalLAstRepaymentPeriod);
List<RepaymentPeriod> repaymentPeriods = updateExpectedRepaymentPeriods.stream().map(e -> {
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc, loanProductRelatedDetail);
rp.setTotalDisbursedAmount(originalLAstRepaymentPeriod.getTotalDisbursedAmount());
rp.setTotalCapitalizedIncomeAmount(originalLAstRepaymentPeriod.getTotalCapitalizedIncomeAmount());
prev.set(rp);
return rp;
}).toList();
scheduleModel.repaymentPeriods().addAll(repaymentPeriods);
}
List<LocalDateInterval> generateAdditionalRepaymentPeriodDueDates(final ProgressiveLoanInterestScheduleModel scheduleModel,
final int periods, final int existingRepayments, Function<Long, LocalDate> repaymentPeriodLengthResolver) {
final List<LocalDateInterval> expectedRepaymentPeriods = new ArrayList<>();
Integer repayEvery = scheduleModel.loanProductRelatedDetail().getRepayEvery();
IntStream.range(existingRepayments, existingRepayments + periods).forEach(
i -> expectedRepaymentPeriods.add(LocalDateInterval.create(repaymentPeriodLengthResolver.apply((long) i * repayEvery),
repaymentPeriodLengthResolver.apply((long) (i + 1) * repayEvery))));
return expectedRepaymentPeriods;
}
private void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final LocalDate interestRateChangeEffectiveDate = operation.getSubmittedOnDate().minusDays(1);
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
@@ -486,7 +541,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopy();
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE
|| operation.getAction() == EmiChangeOperation.Action.ADD_REPAYMENT_PERIODS || scheduleModel.isCopy();
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
calculateOutstandingBalance(scheduleModel);
@@ -932,13 +988,40 @@ public final class ProgressiveEMICalculator implements EMICalculator {
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
Money periodEmiInMultiplesOf = applyInstallmentAmountInMultiplesOf(scheduleModel, periodEmi);
Money remainder = total.minus(periodEmiInMultiplesOf.multipliedBy(repaymentPeriods.size(), mc));
AtomicReference<Money> reallocationAmount = new AtomicReference<>(Money.zero(currency));
repaymentPeriods.forEach(rp -> {
Money emi = rp.equals(lastRepaymentPeriod) ? periodEmiInMultiplesOf.add(remainder) : periodEmiInMultiplesOf;
rp.setEmi(emi);
rp.setOriginalEmi(emi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
if (emi.plus(rp.getTotalCreditedAmount(), mc).plus(rp.getFutureUnrecognizedInterest(), mc)
.isGreaterThanOrEqualTo(rp.getTotalPaidAmount())) {
rp.setEmi(emi);
rp.setOriginalEmi(emi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
} else {
Money adjustment = rp.getTotalPaidAmount()
.minus(emi.plus(rp.getTotalCreditedAmount(), mc).plus(rp.getFutureUnrecognizedInterest(), mc));
reallocationAmount.set(reallocationAmount.get().add(adjustment));
rp.setEmi(rp.getTotalPaidAmount());
rp.setOriginalEmi(rp.getTotalPaidAmount());
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
}
});
if (reallocationAmount.get().isGreaterThanZero()) {
repaymentPeriods.reversed().forEach(rp -> {
if (reallocationAmount.get().isGreaterThanZero() && !rp.isFullyPaid()) {
Money minimumNewEmi = MathUtil.max(rp.getEmi().minus(reallocationAmount.get()), Money.zero(currency), true);
Money alreadyPaidEmi = rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount(), mc)
.minus(rp.getFutureUnrecognizedInterest(), mc);
Money newEmi = MathUtil.max(rp.getCalculatedDueInterest(), MathUtil.max(minimumNewEmi, alreadyPaidEmi, true), true);
reallocationAmount.set(reallocationAmount.get().minus(rp.getEmi().minus(newEmi)));
rp.setEmi(newEmi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
}
});
}
}
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
@@ -33,7 +33,7 @@ public class EmiChangeOperation {
DISBURSEMENT, //
INTEREST_RATE_CHANGE, //
CAPITALIZED_INCOME, //
;
ADD_REPAYMENT_PERIODS, //
}
private final Action action;
@@ -42,22 +42,37 @@ public class EmiChangeOperation {
private final Money amount;
private final BigDecimal interestRate;
private final int periodsToAdd;
public static EmiChangeOperation disburse(final LocalDate disbursementDueDate, final Money disbursedAmount) {
return new EmiChangeOperation(EmiChangeOperation.Action.DISBURSEMENT, disbursementDueDate, disbursedAmount, null);
return new EmiChangeOperation(EmiChangeOperation.Action.DISBURSEMENT, disbursementDueDate, disbursedAmount, null, 0);
}
public static EmiChangeOperation changeInterestRate(final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) {
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate, 0);
}
public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) {
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null);
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null, 0);
}
public static EmiChangeOperation addRepaymentPeriods(final LocalDate transactionDueDate, final Money transactionAmount,
final int numPeriods) {
return new EmiChangeOperation(Action.ADD_REPAYMENT_PERIODS, transactionDueDate, transactionAmount, null, numPeriods);
}
public EmiChangeOperation withZeroAmount() {
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null, 0);
}
return null;
}
public boolean isAddRepaymentPeriods() {
return action == Action.ADD_REPAYMENT_PERIODS;
}
public boolean isInterestRateChange() {
return action == Action.INTEREST_RATE_CHANGE;
}
}
@@ -38,6 +38,7 @@ import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -430,4 +431,13 @@ public class ProgressiveLoanInterestScheduleModel {
public boolean isCopy() {
return this.modifiers.get(COPY);
}
public Function<Long, LocalDate> resolveRepaymentPEriodLengthGeneratorFunction(LocalDate instance) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
case MONTHS -> instance::plusMonths;
case WEEKS -> instance::plusWeeks;
case DAYS -> instance::plusDays;
default -> throw new UnsupportedOperationException();
};
}
}
@@ -3953,6 +3953,225 @@ class ProgressiveEMICalculatorTest {
}
}
@Nested
public class RescheduleInstallmentExtendRepaymentPeriod {
@BeforeEach
void beforeEach() {
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(12.0));
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
}
@Test
public void testExtraTermsDecliningBalance() {
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); //
checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); //
// add extra repayment period 1
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 7, 2), 1);
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
checkPeriod(interestSchedule, 1, 227.81, 6.70, 221.11, 448.87, false); //
checkPeriod(interestSchedule, 2, 227.81, 4.49, 223.32, 225.55, false); //
checkPeriod(interestSchedule, 3, 227.81, 2.26, 225.55, 0.00, false); //
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 8, 2), 2);
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
checkPeriod(interestSchedule, 1, 227.81, 6.70, 221.11, 448.87, false); //
checkPeriod(interestSchedule, 2, 115.04, 4.49, 110.55, 338.32, false); //
checkPeriod(interestSchedule, 3, 115.04, 3.38, 111.66, 226.66, false); //
checkPeriod(interestSchedule, 4, 115.04, 2.27, 112.77, 113.89, false); //
checkPeriod(interestSchedule, 5, 115.03, 1.14, 113.89, 0.00, false); //
}
@Test
public void testExtraTermsFlat() {
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
// add extra repayment period 1
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 7, 2), 1);
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 232.22, 10.00, 222.22, 444.45, false); //
checkPeriod(interestSchedule, 2, 232.22, 10.00, 222.22, 222.23, false); //
checkPeriod(interestSchedule, 3, 232.23, 10.00, 222.23, 0.00, false); //
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 8, 2), 2);
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 232.22, 10.00, 222.22, 444.45, false); //
checkPeriod(interestSchedule, 2, 121.11, 10.00, 111.11, 333.34, false); //
checkPeriod(interestSchedule, 3, 121.11, 10.00, 111.11, 222.23, false); //
checkPeriod(interestSchedule, 4, 121.11, 10.00, 111.11, 111.12, false); //
checkPeriod(interestSchedule, 5, 121.12, 10.00, 111.12, 0.00, false); //
}
@Test
public void testExtraTermsDecliningBalanceWithFullyRepaidPeriods() {
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
checkPeriod(interestSchedule, 0, 256.28, 10.00, 246.28, 753.72, false); //
checkPeriod(interestSchedule, 1, 256.28, 7.54, 248.74, 504.98, false); //
checkPeriod(interestSchedule, 2, 256.28, 5.05, 251.23, 253.75, false); //
checkPeriod(interestSchedule, 3, 256.29, 2.54, 253.75, 0.00, false); //
// simulate merchant issued refund allocated on last installment ( fully repaid ) on first date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(256.29D)));
// simulate early repayment on day 0
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(256.28D)));
// verify 1st and 4th installments are fully repaid
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
checkPeriod(interestSchedule, 1, 256.28, 9.74, 246.54, 240.89, false); //
checkPeriod(interestSchedule, 2, 243.30, 2.41, 240.89, 0.00, false); //
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 0.00, true); //
// add extra repayment period 1
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 1);
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
// period
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
checkPeriod(interestSchedule, 1, 206.04, 9.74, 196.30, 291.13, false); //
checkPeriod(interestSchedule, 2, 206.04, 2.91, 203.13, 88.00, false); //
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 88.00, true); //
checkPeriod(interestSchedule, 4, 89.76, 1.76, 88.00, 0.00, false); //
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 2);
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
// periods
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
checkPeriod(interestSchedule, 1, 126.91, 9.74, 117.17, 370.26, false); //
checkPeriod(interestSchedule, 2, 126.91, 3.70, 123.21, 247.05, false); //
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 247.05, true); //
checkPeriod(interestSchedule, 4, 126.91, 4.94, 121.97, 125.08, false); //
checkPeriod(interestSchedule, 5, 126.33, 1.25, 125.08, 0.00, false); //
checkPeriod(interestSchedule, 6, 0.00, 0.00, 0.00, 0.00, true); //
}
@Test
public void testExtraTermsFlatWithFullyRepaidPeriods() {
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 750.00, false); //
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 500.00, false); //
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 250.00, false); //
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, false); //
// simulate merchant issued refund allocated on last installment ( fully repaid ) on first date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(250.0D)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(10.0D)));
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, false); //
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 250.00, false); //
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 0.00, false); //
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, true); //
// simulate early repayment on day 0
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(250.0D)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
Money.of(currency, BigDecimal.valueOf(10.0D)));
// verify 1st and 4th installments are fully repaid
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 250.00, false); //
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 0.00, false); //
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, true); //
// add extra repayment period 1
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 1);
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
// period
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
checkPeriod(interestSchedule, 1, 210.00, 10.00, 200.00, 300.00, false); //
checkPeriod(interestSchedule, 2, 210.00, 10.00, 200.00, 100.00, false); //
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 100.00, true); //
checkPeriod(interestSchedule, 4, 110.00, 10.00, 100.00, 0.00, false); //
// add extra repayment period 2
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 2);
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
// periods
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
checkPeriod(interestSchedule, 1, 152.86, 10.00, 142.86, 357.14, false); //
checkPeriod(interestSchedule, 2, 152.86, 10.00, 142.86, 214.28, false); //
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 214.28, true); //
checkPeriod(interestSchedule, 4, 152.86, 10.00, 142.86, 71.42, false); //
checkPeriod(interestSchedule, 5, 81.42, 10.00, 71.42, 0.00, false); //
checkPeriod(interestSchedule, 6, 10.00, 10.00, 0.00, 0.00, false); //
}
}
// utilities
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
@@ -395,7 +395,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
.getLoanRescheduleRequestToTermVariationMappings()) {
mapping.getLoanTermVariations().updateIsActive(true);
LoanTermVariationType termType = mapping.getLoanTermVariations().getTermType();
if (termType.isInterestRateVariation() || termType.isInterestRateFromInstallment()) {
if (termType.isInterestRateVariation() || termType.isInterestRateFromInstallment() || termType.isExtendRepaymentPeriod()) {
hasInterestRateChange = true;
}
}