From 039bca440b8194b8da677aa3a24502ecd54c185c Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 23 Apr 2024 18:24:00 +0200 Subject: [PATCH] FINERACT-1971: Fix wrong due date calculation when loan got submitted --- .../domain/LoanApplicationTerms.java | 8 +- .../service/LoanScheduleAssembler.java | 48 ++- .../BaseLoanIntegrationTest.java | 146 +++++---- .../LoanDueCalculationTest.java | 286 ++++++++++++++++++ 4 files changed, 390 insertions(+), 98 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanDueCalculationTest.java 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 94d99025073..df7aa3b70a8 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 @@ private LoanApplicationTerms(final ApplicationCurrency currency, final Integer l 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 ba296caf0bd..3ddff00e378 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 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement * 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 @@ private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Int 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 @@ private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Int // (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 @@ private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Int 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 e78b4bba784..0a6bfeee023 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.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 @@ @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 @@ private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallm 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 @@ protected void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, D 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 00000000000..132ab63869a --- /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 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")) // + ; + }); + } +}