This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 09031aa5a7 FINERACT-2412: Full term tranche - Schedule handling and
Calculations
09031aa5a7 is described below
commit 09031aa5a76d3374e727a40b7f72695ca208c6e4
Author: mariiaKraievska <[email protected]>
AuthorDate: Wed Jan 7 12:29:44 2026 +0200
FINERACT-2412: Full term tranche - Schedule handling and Calculations
---
.../test/data/loanproduct/DefaultLoanProduct.java | 1 +
.../global/LoanProductGlobalInitializerStep.java | 33 +++
.../fineract/test/support/TestContextKey.java | 1 +
.../features/LoanDelayedScheduleCaptures.feature | 139 +++++++++++
.../data/RepaymentScheduleRelatedLoanData.java | 11 +
.../portfolio/loanaccount/domain/Loan.java | 5 +-
.../LoanRepaymentScheduleProcessingWrapper.java | 4 +
.../loanschedule/domain/LoanApplicationTerms.java | 26 ++-
.../domain/LoanRepaymentScheduleModelData.java | 3 +-
.../mapper/LoanTermVariationsMapper.java | 3 +-
.../loanproduct/data/LoanConfigurationDetails.java | 5 +-
.../domain/ILoanConfigurationDetails.java | 2 +
.../misc/Main.java | 2 +-
...ddableProgressiveLoanScheduleGeneratorTest.java | 2 +-
...dvancedPaymentScheduleTransactionProcessor.java | 41 +++-
.../mapper/LoanConfigurationDetailsMapper.java | 3 +-
.../loanproduct/calc/ProgressiveEMICalculator.java | 137 +++++++++--
.../domain/LoanScheduleGeneratorTest.java | 4 +-
.../calc/ProgressiveEMICalculatorTest.java | 95 ++++++++
.../loanaccount/api/LoansApiResource.java | 6 +-
.../service/LoanScheduleAssembler.java | 7 +-
.../service/LoanReadPlatformServiceImpl.java | 2 +-
.../domain/DefaultScheduledDateGeneratorTest.java | 4 +-
.../LoanDisbursementDetailsIntegrationTest.java | 254 +++++++++++++++++++++
24 files changed, 739 insertions(+), 51 deletions(-)
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index 4776e322bb..992a0dae7e 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -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
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index 0fd7cceefd..afe1063e47 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -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,
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index 56e52d42c7..1285378693 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -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";
}
diff --git
a/fineract-e2e-tests-runner/src/test/resources/features/LoanDelayedScheduleCaptures.feature
b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelayedScheduleCaptures.feature
new file mode 100644
index 0000000000..dece132e6d
--- /dev/null
+++
b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelayedScheduleCaptures.feature
@@ -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 |
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java
index c4997ca9d8..3f2b621f91 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java
@@ -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() {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index d8bfda8bc6..fa1984c86b 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -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;
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
index 2ddf2b2465..ba233d7260 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
@@ -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);
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
index fc85244e2f..297ce1b0e4 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
@@ -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() {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
index fe6b1e055a..66fca947cb 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
@@ -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) {
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTermVariationsMapper.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTermVariationsMapper.java
index 7fd5591e75..a4b909c6d6 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTermVariationsMapper.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTermVariationsMapper.java
@@ -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,
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
index a774dae562..977e9a513c 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
@@ -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) {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
index ef5095198f..4ec89f0fd0 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
@@ -73,4 +73,6 @@ public interface ILoanConfigurationDetails {
RecalculationFrequencyType getRestFrequencyType();
LoanPreCloseInterestCalculationStrategy
getPreCloseInterestCalculationStrategy();
+
+ boolean isAllowFullTermForTranche();
}
diff --git
a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java
b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java
index 1b7d006eb6..e3fd40de21 100644
--- a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java
+++ b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java
@@ -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);
diff --git
a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java
b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java
index 0eea0ff98f..7bf3e57311 100644
---
a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java
+++
b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java
@@ -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);
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index a0b789a7fe..52c4a3267b 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -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);
+ }
}
});
}
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
index 9e80895dc0..487c7069ed 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
@@ -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) {
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
index 041e7c97f7..83868d5789 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -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;
}
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
index e192fe5fe1..06d86fe6ad 100644
---
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
@@ -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,
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
index c67c5a8bd0..9fb3cfc26a 100644
---
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -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)");
+ }
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
index 01d0e2edde..306230d682 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
@@ -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()));
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
index 768a38a64b..d07d48b59b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
@@ -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,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index 7003b0b73e..669206db30 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -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
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
index f929fdc8b3..c5fb2085b6 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
@@ -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() {
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDisbursementDetailsIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDisbursementDetailsIntegrationTest.java
index 6d683485b8..291fb332e0 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDisbursementDetailsIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDisbursementDetailsIntegrationTest.java
@@ -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);
+ }
+ }
}