FINERACT-2412: Full term tranche - Schedule handling and Calculations
This commit is contained in:
committed by
Adam Saghy
parent
3e197e367b
commit
09031aa5a7
+1
@@ -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
|
||||
|
||||
+33
@@ -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,
|
||||
|
||||
+1
@@ -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";
|
||||
}
|
||||
|
||||
+139
@@ -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 |
|
||||
+11
@@ -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() {
|
||||
|
||||
+1
-4
@@ -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;
|
||||
}
|
||||
|
||||
+4
@@ -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);
|
||||
|
||||
+19
-7
@@ -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() {
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -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) {
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
+32
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+120
-17
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+95
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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()));
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+254
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user