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 039bca440 FINERACT-1971: Fix wrong due date calculation when loan got
submitted
039bca440 is described below
commit 039bca440b8194b8da677aa3a24502ecd54c185c
Author: Adam Saghy <[email protected]>
AuthorDate: Tue Apr 23 18:24:00 2024 +0200
FINERACT-1971: Fix wrong due date calculation when loan got submitted
---
.../loanschedule/domain/LoanApplicationTerms.java | 8 +-
.../service/LoanScheduleAssembler.java | 48 ++--
.../integrationtests/BaseLoanIntegrationTest.java | 146 ++++++-----
.../integrationtests/LoanDueCalculationTest.java | 286 +++++++++++++++++++++
4 files changed, 390 insertions(+), 98 deletions(-)
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 94d990250..df7aa3b70 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
@@ -431,10 +431,12 @@ public final class LoanApplicationTerms {
this.variationsDataWrapper = new
LoanTermVariationsDataWrapper(loanTermVariations);
this.actualNumberOfRepayments = numberOfRepayments +
getLoanTermVariations().adjustNumberOfRepayments();
this.adjustPrincipalForFlatLoans = principal.zero();
- if (this.calculatedRepaymentsStartingFromDate == null) {
- this.seedDate = this.expectedDisbursementDate;
+ // We only change the seed date if `repaymentStartingFromDate was
provided`
+ if (this.repaymentsStartingFromDate == null) {
+ this.seedDate = repaymentStartDateType.isDisbursementDate() ?
expectedDisbursementDate : submittedOnDate;
} else {
- this.seedDate = this.calculatedRepaymentsStartingFromDate;
+ // When we change the seed date we are taking the
`repaymentsStartingFromDate`
+ this.seedDate = repaymentsStartingFromDate;
}
this.calendarHistoryDataWrapper = calendarHistoryDataWrapper;
this.isInterestChargedFromDateSameAsDisbursalDateEnabled =
isInterestChargedFromDateSameAsDisbursalDateEnabled;
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 ba296caf0..3ddff00e3 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
@@ -274,9 +274,16 @@ public class LoanScheduleAssembler {
* If user has not passed the first repayments date then then derive
the same based on loan type.
*/
if (calculatedRepaymentsStartingFromDate == null) {
+ LocalDate tmpCalculatedRepaymentsStartingFromDate =
deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
+ repaymentPeriodFrequencyType, 0, calendar,
submittedOnDate, repaymentStartDateType);
calculatedRepaymentsStartingFromDate =
deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
repaymentPeriodFrequencyType,
loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar,
submittedOnDate,
repaymentStartDateType);
+ // If calculated repayment start date does not match due to
minimum days between disbursal and first
+ // repayment rule, we set repaymentsStartingFromDate (which will
be used as seed date later)
+ if
(!tmpCalculatedRepaymentsStartingFromDate.equals(calculatedRepaymentsStartingFromDate))
{
+ repaymentsStartingFromDate =
calculatedRepaymentsStartingFromDate;
+ }
}
/*
@@ -1102,13 +1109,12 @@ public class LoanScheduleAssembler {
final RepaymentStartDateType repaymentStartDateType) {
LocalDate derivedFirstRepayment = null;
- final LocalDate
dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(
- repaymentStartDateType) ?
expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment)
: submittedOnDate;
-
+ final LocalDate
dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment =
expectedDisbursementDate
+ .plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
+ final LocalDate seedDate = repaymentStartDateType.isDisbursementDate()
? expectedDisbursementDate : submittedOnDate;
if (calendar != null) {
- derivedFirstRepayment =
deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate,
expectedDisbursementDate,
- repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate,
- repaymentStartDateType);
+ derivedFirstRepayment =
deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate,
seedDate,
+ repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate);
} else { // Individual or group account, or JLG not linked to a meeting
LocalDate dateBasedOnRepaymentFrequency;
// Derive the first repayment date as greater date among
@@ -1116,25 +1122,13 @@ public class LoanScheduleAssembler {
// (disbursement date + minimum between disbursal and first
// repayment )
if (repaymentPeriodFrequencyType.isDaily()) {
- dateBasedOnRepaymentFrequency =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
- ? expectedDisbursementDate.plusDays(repaymentEvery)
- : submittedOnDate.plusDays(repaymentEvery);
-
+ dateBasedOnRepaymentFrequency =
seedDate.plusDays(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isWeekly()) {
- dateBasedOnRepaymentFrequency =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
- ? expectedDisbursementDate.plusWeeks(repaymentEvery)
- : submittedOnDate.plusWeeks(repaymentEvery);
-
+ dateBasedOnRepaymentFrequency =
seedDate.plusWeeks(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isMonthly()) {
- dateBasedOnRepaymentFrequency =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
- ? expectedDisbursementDate.plusMonths(repaymentEvery)
- : submittedOnDate.plusMonths(repaymentEvery);
-
+ dateBasedOnRepaymentFrequency =
seedDate.plusMonths(repaymentEvery);
} else { // yearly loan
- dateBasedOnRepaymentFrequency =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
- ? expectedDisbursementDate.plusYears(repaymentEvery)
- : submittedOnDate.plusYears(repaymentEvery);
-
+ dateBasedOnRepaymentFrequency =
seedDate.plusYears(repaymentEvery);
}
derivedFirstRepayment =
DateUtils.isAfter(dateBasedOnRepaymentFrequency,
dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment) ?
dateBasedOnRepaymentFrequency
@@ -1146,20 +1140,16 @@ public class LoanScheduleAssembler {
private LocalDate deriveFirstRepaymentDateForLoans(final Integer
repaymentEvery, final LocalDate expectedDisbursementDate,
final LocalDate refernceDateForCalculatingFirstRepaymentDate,
final PeriodFrequencyType repaymentPeriodFrequencyType,
- final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final
Calendar calendar, final LocalDate submittedOnDate,
- final RepaymentStartDateType repaymentStartDateType) {
+ final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final
Calendar calendar, final LocalDate submittedOnDate) {
boolean isMeetingSkipOnFirstDayOfMonth =
configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled();
int numberOfDays =
configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue();
final String frequency =
CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
final LocalDate derivedFirstRepayment =
CalendarUtils.getFirstRepaymentMeetingDate(calendar,
refernceDateForCalculatingFirstRepaymentDate, repaymentEvery,
frequency, isMeetingSkipOnFirstDayOfMonth, numberOfDays);
- final LocalDate minimumFirstRepaymentDate =
RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
- ?
expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment)
- : submittedOnDate;
+ final LocalDate minimumFirstRepaymentDate =
expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
return DateUtils.isBefore(minimumFirstRepaymentDate,
derivedFirstRepayment) ? derivedFirstRepayment
: deriveFirstRepaymentDateForLoans(repaymentEvery,
expectedDisbursementDate, derivedFirstRepayment,
- repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate,
- repaymentStartDateType);
+ repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate);
}
private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final
LocalDate disbursalDate, final LocalDate firstRepaymentDate,
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index e78b4bba7..0a6bfeee0 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -91,6 +91,7 @@ import
org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import
org.apache.fineract.integrationtests.useradministration.users.UserHelper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@@ -102,37 +103,21 @@ import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(LoanTestLifecycleExtension.class)
public abstract class BaseLoanIntegrationTest {
+ protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
+
static {
Utils.initializeRESTAssured();
}
- protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
-
protected final ResponseSpecification responseSpec =
createResponseSpecification(Matchers.is(200));
protected final ResponseSpecification responseSpec204 =
createResponseSpecification(Matchers.is(204));
-
+ protected final LoanProductHelper loanProductHelper = new
LoanProductHelper();
private final String fullAdminAuthKey = getFullAdminAuthKey();
-
protected final RequestSpecification requestSpec =
createRequestSpecification(fullAdminAuthKey);
private final String nonByPassUserAuthKey =
getNonByPassUserAuthKey(requestSpec, responseSpec);
-
protected final AccountHelper accountHelper = new
AccountHelper(requestSpec, responseSpec);
- protected final LoanTransactionHelper loanTransactionHelper = new
LoanTransactionHelper(requestSpec, responseSpec);
- protected final LoanProductHelper loanProductHelper = new
LoanProductHelper();
- protected JournalEntryHelper journalEntryHelper = new
JournalEntryHelper(requestSpec, responseSpec);
- protected ClientHelper clientHelper = new ClientHelper(requestSpec,
responseSpec);
- protected SchedulerJobHelper schedulerJobHelper = new
SchedulerJobHelper(requestSpec);
- protected final InlineLoanCOBHelper inlineLoanCOBHelper = new
InlineLoanCOBHelper(requestSpec, responseSpec);
-
- protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
-
- protected final LoanAccountLockHelper loanAccountLockHelper = new
LoanAccountLockHelper(requestSpec,
- createResponseSpecification(Matchers.is(202)));
- protected DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
-
// asset
protected final Account loansReceivableAccount =
accountHelper.createAssetAccount("loanPortfolio");
-
protected final Account interestReceivableAccount =
accountHelper.createAssetAccount("interestReceivable");
protected final Account feeReceivableAccount =
accountHelper.createAssetAccount("feeReceivable");
protected final Account penaltyReceivableAccount =
accountHelper.createAssetAccount("penaltyReceivable");
@@ -146,7 +131,6 @@ public abstract class BaseLoanIntegrationTest {
protected final Account penaltyIncomeAccount =
accountHelper.createIncomeAccount("penaltyIncome");
protected final Account feeChargeOffAccount =
accountHelper.createIncomeAccount("feeChargeOff");
protected final Account penaltyChargeOffAccount =
accountHelper.createIncomeAccount("penaltyChargeOff");
-
protected final Account recoveriesAccount =
accountHelper.createIncomeAccount("recoveries");
protected final Account interestIncomeChargeOffAccount =
accountHelper.createIncomeAccount("interestIncomeChargeOff");
// expense
@@ -154,6 +138,61 @@ public abstract class BaseLoanIntegrationTest {
protected final Account chargeOffFraudExpenseAccount =
accountHelper.createExpenseAccount("chargeOffFraud");
protected final Account writtenOffAccount =
accountHelper.createExpenseAccount();
protected final Account goodwillExpenseAccount =
accountHelper.createExpenseAccount();
+ protected final LoanTransactionHelper loanTransactionHelper = new
LoanTransactionHelper(requestSpec, responseSpec);
+ protected JournalEntryHelper journalEntryHelper = new
JournalEntryHelper(requestSpec, responseSpec);
+ protected ClientHelper clientHelper = new ClientHelper(requestSpec,
responseSpec);
+ protected SchedulerJobHelper schedulerJobHelper = new
SchedulerJobHelper(requestSpec);
+ protected final InlineLoanCOBHelper inlineLoanCOBHelper = new
InlineLoanCOBHelper(requestSpec, responseSpec);
+ protected final LoanAccountLockHelper loanAccountLockHelper = new
LoanAccountLockHelper(requestSpec,
+ createResponseSpecification(Matchers.is(202)));
+ protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
+ protected DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
+
+ protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, LocalDate dueDate, double principalDue,
+ double principalPaid, double principalOutstanding, double
paidInAdvance, double paidLate) {
+ GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
+ .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
+ assertEquals(dueDate, period.getDueDate());
+ assertEquals(principalDue, period.getPrincipalDue());
+ assertEquals(principalPaid, period.getPrincipalPaid());
+ assertEquals(principalOutstanding, period.getPrincipalOutstanding());
+ assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
+ assertEquals(paidLate, period.getTotalPaidLateForPeriod());
+ }
+
+ protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, double principalDue,
+ double principalPaid, double principalOutstanding, double
paidInAdvance, double paidLate) {
+ GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
+ .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
+ assertEquals(principalDue, period.getPrincipalDue());
+ assertEquals(principalPaid, period.getPrincipalPaid());
+ assertEquals(principalOutstanding, period.getPrincipalOutstanding());
+ assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
+ assertEquals(paidLate, period.getTotalPaidLateForPeriod());
+ }
+
+ protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, LocalDate dueDate, double principalDue,
+ double principalPaid, double principalOutstanding, double feeDue,
double feePaid, double feeOutstanding, double penaltyDue,
+ double penaltyPaid, double penaltyOutstanding, double interestDue,
double interestPaid, double interestOutstanding,
+ double paidInAdvance, double paidLate) {
+ GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
+ .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
+ assertEquals(dueDate, period.getDueDate());
+ assertEquals(principalDue, period.getPrincipalDue());
+ assertEquals(principalPaid, period.getPrincipalPaid());
+ assertEquals(principalOutstanding, period.getPrincipalOutstanding());
+ assertEquals(feeDue, period.getFeeChargesDue());
+ assertEquals(feePaid, period.getFeeChargesPaid());
+ assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
+ assertEquals(penaltyDue, period.getPenaltyChargesDue());
+ assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
+ assertEquals(penaltyOutstanding,
period.getPenaltyChargesOutstanding());
+ assertEquals(interestDue, period.getInterestDue());
+ assertEquals(interestPaid, period.getInterestPaid());
+ assertEquals(interestOutstanding, period.getInterestOutstanding());
+ assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
+ assertEquals(paidLate, period.getTotalPaidLateForPeriod());
+ }
private String getNonByPassUserAuthKey(RequestSpecification requestSpec,
ResponseSpecification responseSpec) {
// creates the user
@@ -287,6 +326,27 @@ public abstract class BaseLoanIntegrationTest {
return advancedPaymentData;
}
+ protected PostLoanProductsRequest
create4Period1MonthLongWithoutInterestProduct(String repaymentStrategy) {
+ PostLoanProductsRequest productRequest =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().multiDisburseLoan(false)//
+ .disallowExpectedDisbursements(false)//
+ .allowApprovedDisbursedAmountsOverApplied(false)//
+ .overAppliedCalculationType(null)//
+ .overAppliedNumber(null)//
+ .principal(1000.0)//
+ .numberOfRepayments(4)//
+ .repaymentEvery(1)//
+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
+ .transactionProcessingStrategyCode(repaymentStrategy)//
+ ;
+ if
(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(repaymentStrategy))
{
+
productRequest.loanScheduleType("PROGRESSIVE").loanScheduleProcessingType("HORIZONTAL")
+
.addPaymentAllocationItem(createDefaultPaymentAllocation("NEXT_INSTALLMENT"));
+ } else {
+
productRequest.loanScheduleType("CUMULATIVE").loanScheduleProcessingType(null).paymentAllocation(null);
+ }
+ return productRequest;
+ }
+
protected PostLoanProductsRequest
create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
int interestType, int amortizationType) {
return
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().multiDisburseLoan(false)//
@@ -773,52 +833,6 @@ public abstract class BaseLoanIntegrationTest {
assertEquals(totalOverpaid, loanDetails.getTotalOverpaid());
}
- protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, LocalDate dueDate, double principalDue,
- double principalPaid, double principalOutstanding, double
paidInAdvance, double paidLate) {
- GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
- .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
- assertEquals(dueDate, period.getDueDate());
- assertEquals(principalDue, period.getPrincipalDue());
- assertEquals(principalPaid, period.getPrincipalPaid());
- assertEquals(principalOutstanding, period.getPrincipalOutstanding());
- assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
- assertEquals(paidLate, period.getTotalPaidLateForPeriod());
- }
-
- protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, double principalDue,
- double principalPaid, double principalOutstanding, double
paidInAdvance, double paidLate) {
- GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
- .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
- assertEquals(principalDue, period.getPrincipalDue());
- assertEquals(principalPaid, period.getPrincipalPaid());
- assertEquals(principalOutstanding, period.getPrincipalOutstanding());
- assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
- assertEquals(paidLate, period.getTotalPaidLateForPeriod());
- }
-
- protected static void validateRepaymentPeriod(GetLoansLoanIdResponse
loanDetails, Integer index, LocalDate dueDate, double principalDue,
- double principalPaid, double principalOutstanding, double feeDue,
double feePaid, double feeOutstanding, double penaltyDue,
- double penaltyPaid, double penaltyOutstanding, double interestDue,
double interestPaid, double interestOutstanding,
- double paidInAdvance, double paidLate) {
- GetLoansLoanIdRepaymentPeriod period =
loanDetails.getRepaymentSchedule().getPeriods().stream()
- .filter(p -> Objects.equals(p.getPeriod(),
index)).findFirst().orElseThrow();
- assertEquals(dueDate, period.getDueDate());
- assertEquals(principalDue, period.getPrincipalDue());
- assertEquals(principalPaid, period.getPrincipalPaid());
- assertEquals(principalOutstanding, period.getPrincipalOutstanding());
- assertEquals(feeDue, period.getFeeChargesDue());
- assertEquals(feePaid, period.getFeeChargesPaid());
- assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
- assertEquals(penaltyDue, period.getPenaltyChargesDue());
- assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
- assertEquals(penaltyOutstanding,
period.getPenaltyChargesOutstanding());
- assertEquals(interestDue, period.getInterestDue());
- assertEquals(interestPaid, period.getInterestPaid());
- assertEquals(interestOutstanding, period.getInterestOutstanding());
- assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
- assertEquals(paidLate, period.getTotalPaidLateForPeriod());
- }
-
protected void checkMaturityDates(long loanId, LocalDate
expectedMaturityDate, LocalDate actualMaturityDate) {
GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(loanId);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDueCalculationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDueCalculationTest.java
new file mode 100644
index 000000000..132ab6386
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDueCalculationTest.java
@@ -0,0 +1,286 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static
org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.stream.Stream;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
+import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class LoanDueCalculationTest extends BaseLoanIntegrationTest {
+
+ private static Stream<Arguments> processingStrategy() {
+ return Stream.of(
+ Arguments.of(Named.of("originalStrategy",
+
DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY)),
//
+ Arguments.of(Named.of("advancedStrategy",
AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY))
//
+ );
+ }
+
+ // Repayment dates are calculated from the provided date (2024-02-29). As
repayment starting date was provided, it
+ // overrules `repayment start date type` configuration
+ @ParameterizedTest
+ @MethodSource("processingStrategy")
+ public void dueDateBasedOnFirstRepaymentDate(String repaymentProcessor) {
+ runAt("2 February 2024", () -> {
+ // Client and Loan account creation
+ final Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest loanProductsRequest =
create4Period1MonthLongWithoutInterestProduct(repaymentProcessor);
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(loanProductsRequest);
+
+ PostLoansRequest loanRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), "2024-01-31", 1000.0, 4,
+ (postLoansRequest) -> {
+
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
+
.loanTermFrequency(4).loanTermFrequencyType(2).dateFormat("yyyy-MM-dd")
+ .repaymentsStartingFromDate(LocalDate.of(2024,
2, 29));
+ });
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(loanRequest);
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "29 March 2024"), //
+ installment(250.0, false, "29 April 2024"), //
+ installment(250.0, false, "29 May 2024")) //
+ ;
+
+
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(1000.0, "31 January 2024"));
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "29 March 2024"), //
+ installment(250.0, false, "29 April 2024"), //
+ installment(250.0, false, "29 May 2024")) //
+ ;
+
+ disburseLoan(postLoansResponse.getLoanId(),
BigDecimal.valueOf(1000.00), "31 January 2024");
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "29 March 2024"), //
+ installment(250.0, false, "29 April 2024"), //
+ installment(250.0, false, "29 May 2024")) //
+ ;
+
+ });
+ }
+
+ // Repayment dates are calculated based on `repayment start date type`
configuration(=Expected disbursement date).
+ // Expected disbursement date `2024-01-30`,
+ // which is used to generate repayment due date when loan got submitted
and approved, however the loan got disbursed
+ // on `2024-01-31`,
+ // the repayment schedule reflects the "new date" after it got disbursed
+ @ParameterizedTest
+ @MethodSource("processingStrategy")
+ public void dueDateBasedOnExpectedDisbursementDate(String
repaymentProcessor) {
+ runAt("31 March 2024", () -> {
+ // Client and Loan account creation
+ final Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest loanProductsRequest =
create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
+
.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE.getValue());
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(loanProductsRequest);
+
+ PostLoansRequest loanRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), "2024-01-30", 1000.0, 4,
+ (postLoansRequest) -> {
+
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
+
.loanTermFrequency(4).loanTermFrequencyType(2).dateFormat("yyyy-MM-dd");
+ });
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(loanRequest);
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "30 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "30 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "30 May 2024")) //
+ ;
+
+
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(1000.0, "31 January 2024"));
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "30 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "30 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "30 May 2024")) //
+ ;
+
+ disburseLoan(postLoansResponse.getLoanId(),
BigDecimal.valueOf(1000.00), "31 March 2024");
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "31 May 2024"), //
+ installment(250.0, false, "30 June 2024"), //
+ installment(250.0, false, "31 July 2024")) //
+ ;
+ });
+ }
+
+ // Repayment dates are calculated based on `repayment start date type`
configuration(=Submitted on date). Submitted
+ // on date is `2024-01-31`,
+ // and even the expected disbursement date is `2024-02-01`, the generated
repayment schedule honors the submitted on
+ // date
+ // when it got disbursed on `2024-02-03`, the repayment schedule due dates
got no changed.
+ @ParameterizedTest
+ @MethodSource("processingStrategy")
+ public void dueDateBasedOnSubmittedOnDate(String repaymentProcessor) {
+ runAt("03 February 2024", () -> {
+ // Client and Loan account creation
+ final Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest loanProductsRequest =
create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
+
.repaymentStartDateType(RepaymentStartDateType.SUBMITTED_ON_DATE.getValue());
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(loanProductsRequest);
+
+ PostLoansRequest loanRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), "2024-02-01", 1000.0, 4,
+ (postLoansRequest) -> {
+
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
+
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
+ });
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(loanRequest);
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "31 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "31 May 2024")) //
+ ;
+
+
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(1000.0, "31 January 2024"));
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "31 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "31 May 2024")) //
+ ;
+
+ disburseLoan(postLoansResponse.getLoanId(),
BigDecimal.valueOf(1000.00), "03 February 2024");
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "01 February 2024"), //
+ installment(250.0, false, "29 February 2024"), //
+ installment(250.0, false, "31 March 2024"), //
+ installment(250.0, false, "30 April 2024"), //
+ installment(250.0, false, "31 May 2024")) //
+ ;
+ });
+ }
+
+ // Repayment dates are calculated based on `repayment start date type`
configuration(=Submitted on date). Submitted
+ // on date is `2024-01-31 the expected disbursement date is `2024-02-26`,
the minimum days between disbursement and
+ // first repayment is 10 days
+ // so the repayment schedule got amended accordingly
+ @ParameterizedTest
+ @MethodSource("processingStrategy")
+ public void
dueDateBasedOnSubmittedOnDateButThereShallBeMinimumDaysBetweenDisbursementAndFirstRepayment(String
repaymentProcessor) {
+ runAt("31 January 2024", () -> {
+ // Client and Loan account creation
+ final Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest loanProductsRequest =
create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
+
.repaymentStartDateType(RepaymentStartDateType.SUBMITTED_ON_DATE.getValue())
+ .minimumDaysBetweenDisbursalAndFirstRepayment(10);
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(loanProductsRequest);
+
+ PostLoansRequest loanRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), "2024-02-26", 1000.0, 4,
+ (postLoansRequest) -> {
+
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
+
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
+ });
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(loanRequest);
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+
+
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(1000.0, "31 January 2024"));
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+
+ disburseLoan(postLoansResponse.getLoanId(),
BigDecimal.valueOf(1000.00), "31 January 2024");
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+ });
+ }
+
+ // Repayment dates are calculated based on `repayment start date type`
configuration(=Disbursement date). Submitted
+ // on date is `2024-01-31 the expected disbursement date is `2024-02-26`,
the minimum days between disbursement and
+ // first repayment is 36 days
+ // so the repayment schedule got amended accordingly
+ @ParameterizedTest
+ @MethodSource("processingStrategy")
+ public void
dueDateBasedOnExpectedDisbursalDateButThereShallBeMinimumDaysBetweenDisbursementAndFirstRepayment(
+ String repaymentProcessor) {
+ runAt("31 January 2024", () -> {
+ // Client and Loan account creation
+ final Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest loanProductsRequest =
create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
+
.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE.getValue())
+ .minimumDaysBetweenDisbursalAndFirstRepayment(36);
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(loanProductsRequest);
+
+ PostLoansRequest loanRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), "2024-01-31", 1000.0, 4,
+ (postLoansRequest) -> {
+
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
+
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
+ });
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(loanRequest);
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+
+
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(1000.0, "31 January 2024"));
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+
+ disburseLoan(postLoansResponse.getLoanId(),
BigDecimal.valueOf(1000.00), "31 January 2024");
+
+ verifyRepaymentSchedule(postLoansResponse.getLoanId(),
installment(1000.0, null, "31 January 2024"), //
+ installment(250.0, false, "07 March 2024"), //
+ installment(250.0, false, "07 April 2024"), //
+ installment(250.0, false, "07 May 2024"), //
+ installment(250.0, false, "07 June 2024")) //
+ ;
+ });
+ }
+}