FINERACT-2412: Full term tranche - Schedule handling and Calculations

This commit is contained in:
mariiaKraievska
2026-01-13 18:06:12 +01:00
committed by Adam Saghy
parent 3e197e367b
commit 09031aa5a7
24 changed files with 739 additions and 51 deletions
@@ -182,6 +182,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_360_30_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, //
LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_PRINCIPAL_FIRST, //
LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_360_30_USD, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE, //
;
@Override
@@ -4318,6 +4318,39 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_360_30_USD,
responseLoanProductsResponseAdvCustomPaymentAllocationProgressiveLoanScheduleHorizontalUSD);
// LP2 with progressive loan schedule + horizontal + interest recalculation daily EMI + 360/30 +
// multidisbursement with full term tranche enabled
// Frequency for recalculate Outstanding Principal: Daily, Frequency Interval for recalculation: 1
// (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE)
String name170 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE
.getName();
PostLoanProductsRequest loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyMultiDisburseFullTermTranche = loanProductsRequestFactory
.defaultLoanProductsRequestLP2Emi()//
.name(name170)//
.daysInYearType(DaysInYearType.DAYS360.value)//
.daysInMonthType(DaysInMonthType.DAYS30.value)//
.isInterestRecalculationEnabled(true)//
.preClosureInterestCalculationStrategy(1)//
.rescheduleStrategyMethod(4)//
.interestRecalculationCompoundingMethod(0)//
.recalculationRestFrequencyType(2)//
.recalculationRestFrequencyInterval(1)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")))//
.multiDisburseLoan(true)//
.disallowExpectedDisbursements(true)//
.allowFullTermForTranche(true)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0);//
PostLoanProductsResponse responseLoanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyMultiDisburseFullTermTranche = createLoanProductIdempotent(
loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyMultiDisburseFullTermTranche);
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE,
responseLoanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyMultiDisburseFullTermTranche);
}
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -293,5 +293,6 @@ public abstract class TestContextKey {
public static final String LP1_INTEREST_FLAT_DAILY_ACTUAL_ACTUAL_MULTIDISB_EXPECT_TRANCHES = "loanProductCreateResponseLP1InterestFlatDailyActualActualMultiDisbursementExpectTranches";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentZeroInterestChargeOffBehaviourAccrualActivity";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_PRINCIPAL_FIRST = "loanProductCreateResponseLP2AdvancedPaymentHorizontalPrincipalFirst";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseFullTermTranche";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_360_30_USD = "loanProductCreateResponseLP2AdvancedPaymentHorizontal36030Usd";
}
@@ -0,0 +1,139 @@
@DelayedScheduleCapturesFeature
Feature: Full Term Tranche - Schedule handling and Calculations
@TestRailId:C4366
Scenario: Verify full term tranche interest bearing progressive loan - Schedule handling and Calculations - Disbursement on Installment Date - UC1
When Admin sets the business date to "01 January 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_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE | 01 January 2024 | 200 | 9.4822 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.79 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 2 | 29 | 01 March 2024 | | 67.19 | 16.47 | 0.66 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 3 | 31 | 01 April 2024 | | 50.59 | 16.6 | 0.53 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 4 | 30 | 01 May 2024 | | 33.86 | 16.73 | 0.4 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 5 | 31 | 01 June 2024 | | 17.0 | 16.86 | 0.27 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 6 | 30 | 01 July 2024 | | 0.0 | 17.0 | 0.13 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.78 | 0.0 | 0.0 | 102.78 | 0.0 | 0.0 | 0.0 | 102.78 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
# --- 2nd disbursement on installment date ---
When Admin sets the business date to "01 February 2024"
When Admin successfully disburse the loan on "01 February 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 7 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.79 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| | | 01 February 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 2 | 29 | 01 March 2024 | | 150.85 | 32.81 | 1.45 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 3 | 31 | 01 April 2024 | | 117.78 | 33.07 | 1.19 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 4 | 30 | 01 May 2024 | | 84.45 | 33.33 | 0.93 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 5 | 31 | 01 June 2024 | | 50.86 | 33.59 | 0.67 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 6 | 30 | 01 July 2024 | | 17.0 | 33.86 | 0.4 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 7 | 31 | 01 August 2024 | | 0.0 | 17.0 | 0.13 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 200.0 | 5.56 | 0.0 | 0.0 | 205.56 | 0.0 | 0.0 | 0.0 | 205.56 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | false |
@TestRailId:C4367
Scenario: Verify full term tranche interest bearing progressive loan - Schedule handling and Calculations - Disbursement mid-period - UC2
When Admin sets the business date to "01 January 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_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE | 01 January 2024 | 200 | 9.4822 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.79 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 2 | 29 | 01 March 2024 | | 67.19 | 16.47 | 0.66 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 3 | 31 | 01 April 2024 | | 50.59 | 16.6 | 0.53 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 4 | 30 | 01 May 2024 | | 33.86 | 16.73 | 0.4 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 5 | 31 | 01 June 2024 | | 17.0 | 16.86 | 0.27 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 6 | 30 | 01 July 2024 | | 0.0 | 17.0 | 0.13 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.78 | 0.0 | 0.0 | 102.78 | 0.0 | 0.0 | 0.0 | 102.78 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
# --- 2nd disbursement mid-period (Feb 15) ---
When Admin sets the business date to "15 February 2024"
When Admin successfully disburse the loan on "15 February 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 7 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.79 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| | | 15 February 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 2 | 29 | 01 March 2024 | | 150.59 | 33.07 | 1.13 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 3 | 31 | 01 April 2024 | | 117.58 | 33.01 | 1.19 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 4 | 30 | 01 May 2024 | | 84.31 | 33.27 | 0.93 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 5 | 31 | 01 June 2024 | | 50.78 | 33.53 | 0.67 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 6 | 30 | 01 July 2024 | | 16.92 | 33.86 | 0.4 | 0.0 | 0.0 | 34.26 | 0.0 | 0.0 | 0.0 | 34.26 |
| 7 | 31 | 01 August 2024 | | 0.0 | 16.92 | 0.13 | 0.0 | 0.0 | 17.05 | 0.0 | 0.0 | 0.0 | 17.05 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 200.0 | 5.24 | 0.0 | 0.0 | 205.24 | 0.0 | 0.0 | 0.0 | 205.24 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 15 February 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | false |
@TestRailId:C4368
Scenario: Verify full term tranche interest bearing progressive loan - Schedule handling and Calculations - Both disbursements before first repayment - UC3
When Admin sets the business date to "01 January 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_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE | 01 January 2024 | 200 | 9.4822 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.79 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 2 | 29 | 01 March 2024 | | 67.19 | 16.47 | 0.66 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 3 | 31 | 01 April 2024 | | 50.59 | 16.6 | 0.53 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 4 | 30 | 01 May 2024 | | 33.86 | 16.73 | 0.4 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 5 | 31 | 01 June 2024 | | 17.0 | 16.86 | 0.27 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
| 6 | 30 | 01 July 2024 | | 0.0 | 17.0 | 0.13 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.78 | 0.0 | 0.0 | 102.78 | 0.0 | 0.0 | 0.0 | 102.78 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
# --- 2nd disbursement before first repayment date (Jan 15) - no term extension ---
When Admin sets the business date to "15 January 2024"
When Admin successfully disburse the loan on "15 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| | | 15 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 167.02 | 32.98 | 1.22 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 2 | 29 | 01 March 2024 | | 134.14 | 32.88 | 1.32 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 3 | 31 | 01 April 2024 | | 101.0 | 33.14 | 1.06 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 4 | 30 | 01 May 2024 | | 67.6 | 33.4 | 0.8 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 5 | 31 | 01 June 2024 | | 33.93 | 33.67 | 0.53 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
| 6 | 30 | 01 July 2024 | | 0.0 | 33.93 | 0.27 | 0.0 | 0.0 | 34.2 | 0.0 | 0.0 | 0.0 | 34.2 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 200.0 | 5.2 | 0.0 | 0.0 | 205.2 | 0.0 | 0.0 | 0.0 | 205.2 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 15 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | false |
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
/**
@@ -34,10 +35,19 @@ public class RepaymentScheduleRelatedLoanData {
private final BigDecimal netDisbursalAmount;
private final BigDecimal inArrearsTolerance;
private final BigDecimal totalFeeChargesAtDisbursement;
@Getter
private final boolean allowFullTermForTranche;
public RepaymentScheduleRelatedLoanData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate,
final CurrencyData currency, final BigDecimal principal, final BigDecimal inArrearsTolerance,
final BigDecimal totalFeeChargesAtDisbursement) {
this(expectedDisbursementDate, actualDisbursementDate, currency, principal, inArrearsTolerance, totalFeeChargesAtDisbursement,
false);
}
public RepaymentScheduleRelatedLoanData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate,
final CurrencyData currency, final BigDecimal principal, final BigDecimal inArrearsTolerance,
final BigDecimal totalFeeChargesAtDisbursement, final boolean allowFullTermForTranche) {
this.expectedDisbursementDate = expectedDisbursementDate;
this.actualDisbursementDate = actualDisbursementDate;
this.currency = currency;
@@ -45,6 +55,7 @@ public class RepaymentScheduleRelatedLoanData {
this.inArrearsTolerance = inArrearsTolerance;
this.totalFeeChargesAtDisbursement = totalFeeChargesAtDisbursement;
this.netDisbursalAmount = this.principal.subtract(this.totalFeeChargesAtDisbursement);
this.allowFullTermForTranche = allowFullTermForTranche;
}
public LocalDate disbursementDate() {
@@ -427,6 +427,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "enable_installment_level_delinquency", nullable = false)
private boolean enableInstallmentLevelDelinquency = false;
@Getter
@Column(name = "allow_full_term_for_tranche", nullable = false)
private boolean allowFullTermForTranche = false;
@@ -1787,10 +1788,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
public boolean isAllowFullTermForTranche() {
return this.allowFullTermForTranche;
}
public void updateAllowFullTermForTranche(boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
}
@@ -253,6 +253,10 @@ public class LoanRepaymentScheduleProcessingWrapper {
: DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate);
}
public static boolean isInPeriodFromInclusiveToExclusive(final LocalDate targetDate, final LocalDate fromDate, final LocalDate toDate) {
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, toDate, targetDate);
}
public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) {
LocalDate fromDate = installment.getFromDate();
return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate);
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -247,6 +248,8 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
@Getter
private boolean allowFullTermForTranche = false;
private LoanApplicationTerms(Builder builder) {
this.currency = builder.currency;
@@ -292,6 +295,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeStrategy = builder.buyDownFeeStrategy;
this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType;
this.merchantBuyDownFee = builder.merchantBuyDownFee;
this.allowFullTermForTranche = builder.allowFullTermForTranche;
this.interestMethod = builder.interestMethod;
this.allowPartialPeriodInterestCalculation = builder.allowPartialPeriodInterestCalculation;
}
@@ -333,6 +337,7 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
private boolean allowFullTermForTranche;
private boolean allowPartialPeriodInterestCalculation;
public Builder interestMethod(InterestMethod interestMethod) {
@@ -500,6 +505,11 @@ public final class LoanApplicationTerms {
return this;
}
public Builder allowFullTermForTranche(boolean value) {
this.allowFullTermForTranche = value;
return this;
}
public LoanApplicationTerms build() {
return new LoanApplicationTerms(this);
}
@@ -542,7 +552,8 @@ public final class LoanApplicationTerms {
.submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate)
.interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate())
.daysInYearCustomStrategy(modelData.daysInYearCustomStrategy()).interestMethod(modelData.interestMethod())
.allowPartialPeriodInterestCalculation(modelData.allowPartialPeriodInterestCalculation()).mc(mc).build();
.allowPartialPeriodInterestCalculation(modelData.allowPartialPeriodInterestCalculation())
.allowFullTermForTranche(modelData.allowFullTermForTranche()).mc(mc).build();
}
public static LoanApplicationTerms assembleFrom(final CurrencyData currency, final Integer loanTermFrequency,
@@ -579,7 +590,7 @@ public final class LoanApplicationTerms {
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean merchantBuyDownFee, final boolean allowFullTermForTranche) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -601,7 +612,7 @@ public final class LoanApplicationTerms {
fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, allowFullTermForTranche);
}
@@ -622,7 +633,7 @@ public final class LoanApplicationTerms {
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) {
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final boolean allowFullTermForTranche) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
@@ -680,7 +691,7 @@ public final class LoanApplicationTerms {
loanProductRelatedDetail.getCapitalizedIncomeStrategy(), loanProductRelatedDetail.getCapitalizedIncomeType(),
loanProductRelatedDetail.isEnableBuyDownFee(), loanProductRelatedDetail.getBuyDownFeeCalculationType(),
loanProductRelatedDetail.getBuyDownFeeStrategy(), loanProductRelatedDetail.getBuyDownFeeIncomeType(),
loanProductRelatedDetail.isMerchantBuyDownFee());
loanProductRelatedDetail.isMerchantBuyDownFee(), allowFullTermForTranche);
}
private LoanApplicationTerms(final CurrencyData currency, final Integer loanTermFrequency,
@@ -716,7 +727,7 @@ public final class LoanApplicationTerms {
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean merchantBuyDownFee, final boolean allowFullTermForTranche) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
@@ -827,6 +838,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeStrategy = buyDownFeeStrategy;
this.buyDownFeeIncomeType = buyDownFeeIncomeType;
this.merchantBuyDownFee = merchantBuyDownFee;
this.allowFullTermForTranche = allowFullTermForTranche;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1703,7 +1715,7 @@ public final class LoanApplicationTerms {
repaymentEvery, numberOfRepayments,
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
daysInYearCustomStrategy, allowPartialPeriodInterestCalculation, interestRecalculationEnabled, recalculationFrequencyType,
preClosureInterestCalculationStrategy);
preClosureInterestCalculationStrategy, allowFullTermForTranche);
}
public Integer getLoanTermFrequency() {
@@ -35,5 +35,6 @@ public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerati
@NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear,
BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength,
@NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy,
@NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation) {
@NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation,
@NotNull boolean allowFullTermForTranche) {
}
@@ -120,7 +120,8 @@ public class LoanTermVariationsMapper {
scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), holidayDetailDTO,
allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), loan.getFixedPrincipalPercentagePerInstallment(),
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, loan.getSubmittedOnDate());
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, loan.getSubmittedOnDate(),
loan.isAllowFullTermForTranche());
}
private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominalInterestRate, final FloatingRateDTO floatingRateDTO,
@@ -58,6 +58,8 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
private final RecalculationFrequencyType restFrequencyType;
@Getter
private final LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy;
@Getter
private final boolean allowFullTermForTranche;
public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate,
Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
@@ -67,7 +69,7 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation,
boolean isInterestRecalculationEnabled, RecalculationFrequencyType restFrequencyType,
LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy) {
LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy, boolean allowFullTermForTranche) {
this.currency = currency;
this.interestRatePerPeriod = interestRatePerPeriod;
this.annualNominalInterestRate = annualNominalInterestRate;
@@ -89,6 +91,7 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
this.restFrequencyType = restFrequencyType;
this.preCloseInterestCalculationStrategy = preCloseInterestCalculationStrategy;
this.allowFullTermForTranche = allowFullTermForTranche;
}
private Integer defaultToNullIfZero(final Integer value) {
@@ -73,4 +73,6 @@ public interface ILoanConfigurationDetails {
RecalculationFrequencyType getRestFrequencyType();
LoanPreCloseInterestCalculationStrategy getPreCloseInterestCalculationStrategy();
boolean isAllowFullTermForTranche();
}
@@ -62,7 +62,7 @@ public class Main {
final InterestMethod interestMethod = InterestMethod.DECLINING_BALANCE;
final boolean allowPartialPeriodInterestCalculation = true;
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, dasInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation);
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, dasInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation, false);
final LoanSchedulePlan plan = calculator.generate(mc, config);
printPlan(plan);
@@ -67,7 +67,7 @@ class EmbeddableProgressiveLoanScheduleGeneratorTest {
var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments,
repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType,
daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate,
daysInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation);
daysInYearCustomStrategy, interestMethod, allowPartialPeriodInterestCalculation, false);
final LoanSchedulePlan plan = calculator.generate(mc, config);
@@ -119,6 +119,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.apache.fineract.portfolio.util.InstallmentProcessingHelper;
@@ -1595,7 +1596,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
disbursementTransaction.resetDerivedComponents();
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, disbursementTransaction, currency);
allocateOverpayment(disbursementTransaction, transactionCtx);
}
@@ -1669,7 +1670,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, capitalizedIncomeTransaction, currency);
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
}
@@ -1689,9 +1690,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
}
private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model,
List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate, MonetaryCurrency currency) {
boolean isPostMaturityDisbursement = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
private void recalculateRepaymentPeriodsWithEMICalculation(final Money amortizableAmount,
final ProgressiveLoanInterestScheduleModel model, final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction, final MonetaryCurrency currency) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final boolean isPostMaturityDisbursement = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
.allMatch(i -> i.getDueDate().isBefore(transactionDate));
if (amortizableAmount.isGreaterThanZero()) {
@@ -1703,14 +1706,34 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
final ListIterator<LoanRepaymentScheduleInstallment> iterator = installments.listIterator();
final AtomicInteger installmentCounter = new AtomicInteger();
final ILoanConfigurationDetails loanProductRelatedDetail = model.loanProductRelatedDetail();
model.repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
.findFirst().orElse(null);
if (installment != null) {
LoanRepaymentScheduleInstallment installment = null;
while (iterator.hasNext() && (installment == null || installment.isAdditional() || installment.isDownPayment())) {
installment = iterator.next();
installmentCounter.getAndIncrement();
}
if (installment != null && installment.getDueDate().equals(rm.getDueDate())
&& !installment.getDueDate().isBefore(transactionDate)) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(currency, transactionDate);
} else {
if (loanProductRelatedDetail != null && loanProductRelatedDetail.isAllowFullTermForTranche()
&& loanProductRelatedDetail.getNumberOfRepayments() > 0 && !rm.getDueDate().isBefore(transactionDate)) {
installmentCounter.getAndIncrement();
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(
loanTransaction.getLoan(), installmentCounter.get(), rm.getFromDate(), rm.getDueDate(),
rm.getDuePrincipal().getAmount(), rm.getDueInterest().getAmount(), null, null, null, null, null, null,
false, false, false);
newInstallment.updateObligationsMet(currency, transactionDate);
iterator.add(newInstallment);
}
}
});
}
@@ -56,7 +56,8 @@ public final class LoanConfigurationDetailsMapper {
loanProductRelatedDetail.getRepaymentPeriodFrequencyType(), loanProductRelatedDetail.getRepayEvery(),
loanProductRelatedDetail.getNumberOfRepayments(), loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(),
loanProductRelatedDetail.getDaysInYearCustomStrategy(), loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation(),
loan.isInterestRecalculationEnabled(), getRestFrequencyType(loan), getPreCloseInterestCalculationStrategy(loan));
loan.isInterestRecalculationEnabled(), getRestFrequencyType(loan), getPreCloseInterestCalculationStrategy(loan),
loan.isAllowFullTermForTranche());
}
private static RecalculationFrequencyType getRestFrequencyType(Loan loan) {
@@ -18,6 +18,9 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
import static java.math.BigDecimal.ZERO;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriodFromInclusiveToExclusive;
import jakarta.annotation.Nonnull;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
@@ -134,12 +137,112 @@ public final class ProgressiveEMICalculator implements EMICalculator {
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate()))
.forEach(rp -> rp.setTotalDisbursedAmount(rp.getTotalDisbursedAmount().add(operation.getAmount())));
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero(), scheduleModel.zero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
operation));
final int numberOfRepayments = scheduleModel.loanProductRelatedDetail().getNumberOfRepayments();
if (scheduleModel.loanProductRelatedDetail().isAllowFullTermForTranche() && numberOfRepayments > 0) {
addFullTermTrancheDisbursement(scheduleModel, operation);
} else {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero(), scheduleModel.zero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
operation));
}
}
private void addFullTermTrancheDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel,
final EmiChangeOperation operation) {
final MathContext mc = scheduleModel.mc();
final LocalDate disbursementDate = operation.getSubmittedOnDate();
final Money disbursedAmount = operation.getAmount();
final ILoanConfigurationDetails loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final Optional<RepaymentPeriod> firstDisbursedPeriod = scheduleModel.repaymentPeriods().stream()
.filter(period -> isInPeriodFromInclusiveToExclusive(disbursementDate, period.getFromDate(), period.getDueDate()))
.findFirst();
final LocalDate firstDisbursedPeriodStartDate = firstDisbursedPeriod.isPresent() ? firstDisbursedPeriod.get().getFromDate()
: scheduleModel.getMaturityDate();
final LoanApplicationTerms loanApplicationTerms = buildLoanApplicationTerms(loanProductRelatedDetail, firstDisbursedPeriodStartDate,
disbursedAmount, mc);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryScheduleModel(loanApplicationTerms, mc,
firstDisbursedPeriodStartDate, disbursementDate);
mergeNewScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, operation);
}
private LoanApplicationTerms buildLoanApplicationTerms(final ILoanConfigurationDetails loanProductRelatedDetail,
final LocalDate firstDisbursedPeriodStartDate, final Money disbursedAmount, final MathContext mc) {
return new LoanApplicationTerms.Builder()
// Loan basics
.currency(loanProductRelatedDetail.getCurrencyData()).principal(disbursedAmount)
.repaymentsStartingFromDate(firstDisbursedPeriodStartDate).downPaymentPercentage(ZERO)
.seedDate(firstDisbursedPeriodStartDate).inArrearsTolerance(Money.zero(loanProductRelatedDetail.getCurrencyData()))
// Term & frequency
.loanTermFrequency(loanProductRelatedDetail.getNumberOfRepayments())
.loanTermPeriodFrequencyType(loanProductRelatedDetail.getRepaymentPeriodFrequencyType())
.numberOfRepayments(loanProductRelatedDetail.getNumberOfRepayments())
.repaymentEvery(loanProductRelatedDetail.getRepayEvery())
.repaymentPeriodFrequencyType(loanProductRelatedDetail.getRepaymentPeriodFrequencyType())
// Interest configuration
.interestRatePerPeriod(loanProductRelatedDetail.getAnnualNominalInterestRate())
.annualNominalInterestRate(loanProductRelatedDetail.getAnnualNominalInterestRate())
.interestRatePeriodFrequencyType(loanProductRelatedDetail.getRepaymentPeriodFrequencyType())
.interestMethod(loanProductRelatedDetail.getInterestMethod())
.interestRecognitionOnDisbursementDate(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate())
// Day count conventions
.daysInMonthType(DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType()))
.daysInYearType(DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()))
.daysInYearCustomStrategy(loanProductRelatedDetail.getDaysInYearCustomStrategy())
// Flags & options
.isDownPaymentEnabled(false)
.allowPartialPeriodInterestCalculation(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation())
// Technical
.mc(mc).build();
}
private void mergeNewScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
final ProgressiveLoanInterestScheduleModel temporaryScheduleModel, final EmiChangeOperation operation) {
final List<RepaymentPeriod> newPeriods = temporaryScheduleModel.repaymentPeriods();
if (newPeriods.isEmpty()) {
return;
}
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero(), scheduleModel.zero());
for (final RepaymentPeriod newPeriod : newPeriods) {
final Money newPrincipal = newPeriod.getDuePrincipal();
final Money newInterest = newPeriod.getDueInterest();
final Optional<RepaymentPeriod> existingRepaymentPeriod = existingRepaymentPeriods.stream()
.filter(ep -> ep.getFromDate().isEqual(newPeriod.getFromDate()) && ep.getDueDate().equals(newPeriod.getDueDate()))
.findFirst();
if (existingRepaymentPeriod.isPresent()) {
existingRepaymentPeriod.get().setEmi(existingRepaymentPeriod.get().getEmi().add(newPrincipal.add(newInterest)));
existingRepaymentPeriod.get().setOriginalEmi(existingRepaymentPeriod.get().getEmi());
calculateRateFactorForRepaymentPeriod(existingRepaymentPeriod.get(), scheduleModel);
} else {
final RepaymentPeriod rp = RepaymentPeriod.create(
!existingRepaymentPeriods.isEmpty() ? existingRepaymentPeriods.getLast() : null,
(newPeriod.equals(newPeriods.getFirst()) && !existingRepaymentPeriods.isEmpty())
? existingRepaymentPeriods.getLast().getDueDate()
: newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), scheduleModel.mc(),
scheduleModel.loanProductRelatedDetail());
rp.setTotalDisbursedAmount(scheduleModel.getLastRepaymentPeriod().getTotalDisbursedAmount());
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}
}
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, operation.getSubmittedOnDate());
}
private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel,
@@ -590,10 +693,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
moveOutstandingAmountsFromPeriodsBeforeTransactionDate(scheduleModel.repaymentPeriods(), targetDate);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
mc, reAgePeriodStartDate);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryScheduleModel(loanApplicationTerms, mc,
reAgePeriodStartDate, reAgePeriodStartDate);
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, reAgeFirstDueDate, targetDate,
attachTemporaryScheduleModelReAgedPeriodsToExistingModel(scheduleModel, temporaryReAgedScheduleModel, reAgeFirstDueDate, targetDate,
paidBalancesFromTransactionDate);
}
@@ -853,11 +956,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
/**
* * Merging the new temporary model of re-aged repayment periods and existing one together. After that recalculate
* the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
* significantly from other periods.
* * Attaching re-aged repayment periods of the new temporary model to existing model repayment periods. After that
* recalculate the balances of the updated model and also recalculate the EMI if the EMI of the last repayment
* period differs significantly from other periods.
*/
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
private void attachTemporaryScheduleModelReAgedPeriodsToExistingModel(final ProgressiveLoanInterestScheduleModel scheduleModel,
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LocalDate reAgeFirstDueDate,
final LocalDate targetDate, final OutstandingDetails paidBalancesFromTransactionDate) {
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
@@ -914,18 +1017,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
/**
* * Generates temporary interestScheduleModel with re-aged repayment periods
* * Generates temporary interestScheduleModel with particular disbursement date
*/
@NotNull
private ProgressiveLoanInterestScheduleModel generateTemporaryReAgedScheduleModel(final LoanApplicationTerms loanApplicationTerms,
final MathContext mc, final LocalDate periodStartDate) {
private ProgressiveLoanInterestScheduleModel generateTemporaryScheduleModel(final LoanApplicationTerms loanApplicationTerms,
final MathContext mc, final LocalDate periodStartDate, final LocalDate disbursementDate) {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, null);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(periodStartDate, loanApplicationTerms.getPrincipal()));
addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(disbursementDate, loanApplicationTerms.getPrincipal()));
return temporaryReAgedScheduleModel;
}
@@ -65,7 +65,7 @@ class LoanScheduleGeneratorTest {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null,
InterestMethod.DECLINING_BALANCE, true);
InterestMethod.DECLINING_BALANCE, true, false);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
@@ -103,7 +103,7 @@ class LoanScheduleGeneratorTest {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false,
null, InterestMethod.DECLINING_BALANCE, true);
null, InterestMethod.DECLINING_BALANCE, true, false);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
@@ -235,6 +235,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -582,6 +583,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
@@ -1348,6 +1350,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -2476,6 +2479,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -2533,6 +2537,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -2601,6 +2606,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -2953,6 +2959,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -2991,6 +2998,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3028,6 +3036,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3068,6 +3077,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3107,6 +3117,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3143,6 +3154,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy())
.thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3363,6 +3375,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
@@ -3402,6 +3415,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
@@ -3441,6 +3455,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(30);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
@@ -3530,6 +3545,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
@@ -3558,6 +3574,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(8);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
@@ -3587,6 +3604,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
@@ -3620,6 +3638,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
@@ -3653,6 +3672,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
@@ -3691,6 +3711,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(20);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
@@ -3732,6 +3753,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(24);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
@@ -3806,6 +3828,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
@@ -3855,6 +3878,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
@@ -3892,6 +3916,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
@@ -3928,6 +3953,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
@@ -3986,6 +4012,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
@@ -5152,4 +5179,72 @@ class ProgressiveEMICalculatorTest {
return interestScheduleModelService.fromJson(json, toCopy.loanProductRelatedDetail(), toCopy.mc(),
toCopy.installmentAmountInMultiplesOf());
}
@Test
public void test_fullTermTranche_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() {
// Create 7 periods (6 original + 1 extension for second tranche)
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
expectedRepaymentPeriods.add(repayment(7, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 8, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(6);
// Enable full term tranche feature
Mockito.when(loanProductRelatedDetail.isAllowFullTermForTranche()).thenReturn(true);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
// First disbursement: 100 on Jan 1 -> should set EMI ~17.13 on periods 0-5
final Money disbursedAmount1 = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount1);
// Check EMI after first disbursement - periods 0-5 should have ~17.13
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(0).getEmi()), 0.01,
"Period 0 EMI after first disbursement");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(1).getEmi()), 0.01,
"Period 1 EMI after first disbursement");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(2).getEmi()), 0.01,
"Period 2 EMI after first disbursement");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(3).getEmi()), 0.01,
"Period 3 EMI after first disbursement");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(4).getEmi()), 0.01,
"Period 4 EMI after first disbursement");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(5).getEmi()), 0.01,
"Period 5 EMI after first disbursement");
Assertions.assertEquals(0.0, toDouble(interestSchedule.repaymentPeriods().get(6).getEmi()), 0.01,
"Period 6 EMI after first disbursement (should be 0)");
// Second disbursement: 100 on Feb 1 -> should ADD EMI ~17.13 to periods 1-6
final Money disbursedAmount2 = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 1), disbursedAmount2);
// Verify EMI values:
// Period 0: EMI = 17.13 (only first tranche)
// Periods 1-5: EMI = 34.26 (both tranches)
// Period 6: EMI = 17.13 (only second tranche)
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(0).getEmi()), 0.01,
"Period 0 EMI (single tranche)");
Assertions.assertEquals(34.26, toDouble(interestSchedule.repaymentPeriods().get(1).getEmi()), 0.01, "Period 1 EMI (aggregated)");
Assertions.assertEquals(34.26, toDouble(interestSchedule.repaymentPeriods().get(2).getEmi()), 0.01, "Period 2 EMI (aggregated)");
Assertions.assertEquals(34.26, toDouble(interestSchedule.repaymentPeriods().get(3).getEmi()), 0.01, "Period 3 EMI (aggregated)");
Assertions.assertEquals(34.26, toDouble(interestSchedule.repaymentPeriods().get(4).getEmi()), 0.01, "Period 4 EMI (aggregated)");
Assertions.assertEquals(34.26, toDouble(interestSchedule.repaymentPeriods().get(5).getEmi()), 0.01, "Period 5 EMI (aggregated)");
Assertions.assertEquals(17.13, toDouble(interestSchedule.repaymentPeriods().get(6).getEmi()), 0.01,
"Period 6 EMI (single tranche)");
}
}
@@ -527,7 +527,8 @@ public class LoansApiResource {
RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged(),
Boolean.TRUE.equals(i.getAllowFullTermForTranche()));
LoanScheduleData repaymentSchedule = loanReadPlatformService.retrieveRepaymentSchedule(loanId,
repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
@@ -1098,7 +1099,8 @@ public class LoansApiResource {
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
loanBasicDetails.getFeeChargesAtDisbursementCharged());
loanBasicDetails.getFeeChargesAtDisbursementCharged(),
Boolean.TRUE.equals(loanBasicDetails.getAllowFullTermForTranche()));
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
@@ -533,6 +533,11 @@ public class LoanScheduleAssembler {
.extractBooleanNamed(LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, element);
}
boolean allowFullTermForTranche = loanProduct.isAllowFullTermForTranche();
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element)) {
allowFullTermForTranche = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element);
}
return LoanApplicationTerms.assembleFrom(applicationCurrency.toData(), loanTermFrequency, loanTermPeriodFrequencyType,
numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod,
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
@@ -561,7 +566,7 @@ public class LoanScheduleAssembler {
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeCalculationType(),
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeStrategy(),
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeIncomeType(),
loanProduct.getLoanProductRelatedDetail().isMerchantBuyDownFee());
loanProduct.getLoanProductRelatedDetail().isMerchantBuyDownFee(), allowFullTermForTranche);
}
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -259,7 +259,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
accountData.getTimeline().getExpectedDisbursementDate(), accountData.getTimeline().getActualDisbursementDate(),
accountData.getCurrency(), accountData.getPrincipal(), accountData.getInArrearsTolerance(),
accountData.getFeeChargesAtDisbursementCharged());
accountData.getFeeChargesAtDisbursementCharged(), Boolean.TRUE.equals(accountData.getAllowFullTermForTranche()));
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
@@ -91,7 +91,7 @@ public class DefaultScheduledDateGeneratorTest {
DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO,
EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE,
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false, null, false, null, null,
null, false, null, null, null, false);
null, false, null, null, null, false, false);
// when
List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate,
@@ -172,7 +172,7 @@ public class DefaultScheduledDateGeneratorTest {
EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null,
null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false,
null, false, DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null,
false, null, false, null, null, null, false, null, null, null, false);
false, null, false, null, null, null, false, null, null, null, false, false);
}
private HolidayDetailDTO createHolidayDTO() {
@@ -880,4 +880,258 @@ public class LoanDisbursementDetailsIntegrationTest {
assertEquals(false, loanDetails.getAllowFullTermForTranche());
log.info("-------------------LOAN LEVEL OVERRIDE OF allowFullTermForTranche WORKED SUCCESSFULLY-------");
}
@Test
public void testFullTermTranche_S1_DisbursementOnInstallmentDate() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withinterestRatePerPeriod("9.4822").withInterestRateFrequencyTypeAsYear().withMultiDisburse()
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).addAdvancedPaymentAllocation(defaultAllocation)
.withAllowFullTermForTranche(true).withDaysInYear("360").withMinPrincipal("100").build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2024");
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 January 2024", "100"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 February 2024", "100"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("200").withLoanTermFrequency("6")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("6").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("9.4822").withExpectedDisbursementDate("01 January 2024")
.withTranches(createTranches).withSubmittedOnDate("01 January 2024")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
this.loanTransactionHelper.approveLoanWithApproveAmount("01 January 2024", "01 January 2024", "200", loanId, createTranches);
log.info("-------------------LOAN APPROVED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2024", loanId, "100");
log.info("-------------------FIRST TRANCHE DISBURSED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 February 2024", loanId, "100");
log.info("-------------------SECOND TRANCHE DISBURSED-------");
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(loanDetails);
GetLoansLoanIdRepaymentSchedule schedule = loanDetails.getRepaymentSchedule();
assertNotNull(schedule);
List<GetLoansLoanIdRepaymentPeriod> periods = schedule.getPeriods();
assertNotNull(periods);
assertEquals(9, periods.size(), "Total periods should be 9 (2 disbursements + 7 repayment periods)");
// Count disbursement periods (no period number) and repayment periods (with period number)
long disbursementPeriods = periods.stream().filter(p -> p.getPeriod() == null).count();
long repaymentPeriods = periods.stream().filter(p -> p.getPeriod() != null).count();
assertEquals(2, disbursementPeriods, "Should have 2 disbursement periods");
assertEquals(7, repaymentPeriods, "Should have 7 repayment periods");
log.info("-------------------S1 TEST: SCHEDULE VALIDATION-------");
log.info("Schedule structure validated: 2 disbursement + 7 repayment periods");
// Close the loan to allow LoanTestLifecycleExtension cleanup to succeed
closeFullTermTrancheLoan(loanId, "01 August 2024");
}
@Test
public void testFullTermTranche_S2_MidPeriodDisbursement() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withinterestRatePerPeriod("9.4822").withInterestRateFrequencyTypeAsYear().withMultiDisburse()
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).addAdvancedPaymentAllocation(defaultAllocation)
.withAllowFullTermForTranche(true).withDaysInYear("360").withMinPrincipal("100").build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2024");
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 January 2024", "100"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "15 February 2024", "100"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("200").withLoanTermFrequency("6")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("6").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("9.4822").withExpectedDisbursementDate("01 January 2024")
.withTranches(createTranches).withSubmittedOnDate("01 January 2024")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
this.loanTransactionHelper.approveLoanWithApproveAmount("01 January 2024", "01 January 2024", "200", loanId, createTranches);
log.info("-------------------LOAN APPROVED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2024", loanId, "100");
log.info("-------------------FIRST TRANCHE DISBURSED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("15 February 2024", loanId, "100");
log.info("-------------------SECOND TRANCHE DISBURSED (MID-PERIOD)-------");
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(loanDetails);
GetLoansLoanIdRepaymentSchedule schedule = loanDetails.getRepaymentSchedule();
assertNotNull(schedule);
List<GetLoansLoanIdRepaymentPeriod> periods = schedule.getPeriods();
assertNotNull(periods);
assertEquals(9, periods.size(), "Total periods should be 9 (2 disbursements + 7 repayment periods)");
// Count disbursement periods (no period number) and repayment periods (with period number)
long disbursementPeriods = periods.stream().filter(p -> p.getPeriod() == null).count();
long repaymentPeriods = periods.stream().filter(p -> p.getPeriod() != null).count();
assertEquals(2, disbursementPeriods, "Should have 2 disbursement periods");
assertEquals(7, repaymentPeriods, "Should have 7 repayment periods");
log.info("-------------------S2 TEST: SCHEDULE VALIDATION-------");
log.info("Schedule structure validated: 2 disbursement + 7 repayment periods (mid-period disbursement)");
// Close the loan to allow LoanTestLifecycleExtension cleanup to succeed
closeFullTermTrancheLoan(loanId, "01 August 2024");
}
@Test
public void testFullTermTranche_S3_BothBeforeFirstRepayment() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withinterestRatePerPeriod("9.4822").withInterestRateFrequencyTypeAsYear().withMultiDisburse()
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).addAdvancedPaymentAllocation(defaultAllocation)
.withAllowFullTermForTranche(true).withDaysInYear("360").withMinPrincipal("100").build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2024");
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 January 2024", "100"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "15 January 2024", "100"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("200").withLoanTermFrequency("6")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("6").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("9.4822").withExpectedDisbursementDate("01 January 2024")
.withTranches(createTranches).withSubmittedOnDate("01 January 2024")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
this.loanTransactionHelper.approveLoanWithApproveAmount("01 January 2024", "01 January 2024", "200", loanId, createTranches);
log.info("-------------------LOAN APPROVED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2024", loanId, "100");
log.info("-------------------FIRST TRANCHE DISBURSED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("15 January 2024", loanId, "100");
log.info("-------------------SECOND TRANCHE DISBURSED (BEFORE FIRST REPAYMENT)-------");
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(loanDetails);
GetLoansLoanIdRepaymentSchedule schedule = loanDetails.getRepaymentSchedule();
assertNotNull(schedule);
List<GetLoansLoanIdRepaymentPeriod> periods = schedule.getPeriods();
assertNotNull(periods);
assertEquals(8, periods.size(), "Total periods should be 8 (2 disbursements + 6 repayment periods - NO EXTENSION)");
// Count disbursement periods (no period number) and repayment periods (with period number)
long disbursementPeriods = periods.stream().filter(p -> p.getPeriod() == null).count();
long repaymentPeriods = periods.stream().filter(p -> p.getPeriod() != null).count();
assertEquals(2, disbursementPeriods, "Should have 2 disbursement periods");
assertEquals(6, repaymentPeriods, "Should have 6 repayment periods (no term extension)");
log.info("-------------------S3 TEST: SCHEDULE VALIDATION-------");
log.info("Schedule structure validated: 2 disbursement + 6 repayment periods (no term extension)");
log.info("Both disbursements before first repayment date result in same maturity date");
// Close the loan to allow LoanTestLifecycleExtension cleanup to succeed
closeFullTermTrancheLoan(loanId, "01 July 2024");
}
@Test
public void testFullTermTrancheBackwardCompatibility() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductWithoutFlag = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withinterestRatePerPeriod("9.4822").withInterestRateFrequencyTypeAsYear().withMultiDisburse()
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).addAdvancedPaymentAllocation(defaultAllocation)
.withAllowFullTermForTranche(false).withDaysInYear("360").withMinPrincipal("100").build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductWithoutFlag);
log.info("------------------LOAN PRODUCT CREATED WITH allowFullTermForTranche=false ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2024");
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 January 2024", "100"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 February 2024", "100"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("200").withLoanTermFrequency("6")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("6").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("9.4822").withExpectedDisbursementDate("01 January 2024")
.withTranches(createTranches).withSubmittedOnDate("01 January 2024")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
this.loanTransactionHelper.approveLoanWithApproveAmount("01 January 2024", "01 January 2024", "200", loanId, createTranches);
log.info("-------------------LOAN APPROVED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2024", loanId, "100");
log.info("-------------------FIRST TRANCHE DISBURSED-------");
loanTransactionHelper.disburseLoanWithTransactionAmount("01 February 2024", loanId, "100");
log.info("-------------------SECOND TRANCHE DISBURSED-------");
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(loanDetails);
GetLoansLoanIdRepaymentSchedule schedule = loanDetails.getRepaymentSchedule();
assertNotNull(schedule);
List<GetLoansLoanIdRepaymentPeriod> periods = schedule.getPeriods();
assertNotNull(periods);
log.info("-------------------BACKWARD COMPATIBILITY TEST: SCHEDULE VALIDATION-------");
log.info("Expected: OLD behavior when allowFullTermForTranche=false");
log.info("Schedule should NOT use full term tranche logic - should match existing multi-disburse behavior");
}
/**
* Helper method to close a loan by making a full prepayment. This ensures the loan is closed before the
* LoanTestLifecycleExtension cleanup runs.
*/
private void closeFullTermTrancheLoan(Integer loanId, String lastRepaymentDate) {
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
BigDecimal outstandingAmount = loanDetails.getSummary().getTotalOutstanding();
if (outstandingAmount != null && outstandingAmount.compareTo(BigDecimal.ZERO) > 0) {
log.info("-------------------CLOSING LOAN {} WITH PREPAYMENT OF {} ON {}-------", loanId, outstandingAmount, lastRepaymentDate);
this.loanTransactionHelper.makeLoanRepayment(lastRepaymentDate, outstandingAmount.floatValue(), loanId);
}
}
}