From 4a0c52ea8b258e8f296d1afd714166022b515c7d Mon Sep 17 00:00:00 2001 From: Andrii Kulminskyi Date: Mon, 3 Feb 2025 16:51:26 +0200 Subject: [PATCH] FINERACT-2081: Accrual not created for closed or overpaid loans --- .../LoanAccrualsProcessingServiceImpl.java | 18 ++-- .../AccrualsOnLoanClosureTest.java | 91 +++++++++++++++++++ .../BaseLoanIntegrationTest.java | 1 + 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index c740f431f..a2e90154d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -318,7 +318,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic); + AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic, isFinal); boolean mergeTransactions = isFinal || progressiveAccrual; MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); List accrualTransactions = new ArrayList<>(); @@ -393,7 +393,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private AccrualPeriodsData calculateAccrualAmounts(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean periodic) { + private AccrualPeriodsData calculateAccrualAmounts(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean periodic, boolean isFinal) { boolean chargeOnDueDate = isChargeOnDueDate(); LoanProductRelatedDetail productDetail = loan.getLoanProductRelatedDetail(); MonetaryCurrency currency = productDetail.getCurrency(); @@ -402,11 +402,12 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); LocalDate interestCalculationTillDate = loan.isProgressiveSchedule() && loan.getLoanProductRelatedDetail().isInterestRecognitionOnDisbursementDate() ? tillDate.plusDays(1L) : tillDate; - List installments = getInstallmentsToAccrue(loan, interestCalculationTillDate, periodic); + List installments = isFinal ? loan.getRepaymentScheduleInstallments() + : getInstallmentsToAccrue(loan, interestCalculationTillDate, periodic); AccrualPeriodsData accrualPeriods = AccrualPeriodsData.create(installments, firstInstallmentNumber, currency); for (LoanRepaymentScheduleInstallment installment : installments) { addInterestAccrual(loan, interestCalculationTillDate, scheduleGenerator, installment, accrualPeriods); - addChargeAccrual(loan, tillDate, chargeOnDueDate, installment, accrualPeriods); + addChargeAccrual(loan, tillDate, chargeOnDueDate, installment, accrualPeriods, isFinal); } return accrualPeriods; } @@ -512,12 +513,12 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } private void addChargeAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean chargeOnDueDate, - @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods, boolean isFinal) { AccrualPeriodData period = accrualPeriods.getPeriodByInstallmentNumber(installment.getInstallmentNumber()); LocalDate dueDate = installment.getDueDate(); - List loanCharges = loan - .getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate) - : isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod()))); + Collection loanCharges; + loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate) + : isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod()))); for (LoanCharge loanCharge : loanCharges) { addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods); } @@ -596,6 +597,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing LocalDate fromDate = installment.getFromDate(); LocalDate dueDate = installment.getDueDate(); LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; + chargeOnDueDate = chargeOnDueDate || loanCharge.getDueLocalDate().isBefore(loanCharge.getSubmittedOnDate()); return chargeOnDueDate ? loanCharge.isDueInPeriod(fromDate, toDate, isFirstPeriod) : isInPeriod(loanCharge.getSubmittedOnDate(), fromDate, toDate, isFirstPeriod); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java new file mode 100644 index 000000000..1f04e8d77 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java @@ -0,0 +1,91 @@ +/** + * 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.infrastructure.configuration.api.GlobalConfigurationConstants.CHARGE_ACCRUAL_DATE; + +import java.math.BigDecimal; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostChargesRequest; +import org.apache.fineract.client.models.PostChargesResponse; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@Slf4j +public class AccrualsOnLoanClosureTest extends BaseLoanIntegrationTest { + + private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + + private static final String startDate = "22 April 2025"; + private static final String disbursementDate = "22 April 2024"; + private static final String repaymentDate = "25 April 2024"; + private static final Double disbursementAmount = 800.0; + private static final Double repaymentAmount = 820.0; + private static final Double chargeAmount = 20.0; + + private static Long loanId; + private static PostChargesResponse penaltyResponse; + private static final String penaltyCharge1AddedDate = "24 April 2024"; + + @Test + public void testAccrualCreatedOnLoanClosureWithSubmittedDate() { + PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()); + + loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), disbursementDate, disbursementAmount); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(disbursementAmount), disbursementDate); + + penaltyResponse = chargesHelper.createCharges(new PostChargesRequest().active(true).chargeTimeType(2).chargeAppliesTo(1) + .chargeCalculationType(1).penalty(true).amount(20.0).currencyCode("USD").locale("en").chargePaymentMode(0) + .name(Utils.randomStringGenerator("PENALTY_", 6))); + runAt(startDate, () -> { + globalConfigurationHelper.updateGlobalConfiguration(CHARGE_ACCRUAL_DATE, + new PutGlobalConfigurationsRequest().stringValue("submitted-date")); + + loanTransactionHelper.addLoanCharge(loanId, new PostLoansLoanIdChargesRequest().dateFormat("dd MMMM yyyy").locale("en") + .chargeId(penaltyResponse.getResourceId()).amount(chargeAmount).dueDate(penaltyCharge1AddedDate)); + + loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate(repaymentDate).locale("en").transactionAmount(repaymentAmount)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + verifyRepaymentSchedule(loanId, // + installment(800.0, null, "22 April 2024"), // + installment(800.0, 0.0, 0.0, 20.0, 0.0, true, "22 May 2024")); + + verifyTransactions(loanId, // + transaction(800.0, "Disbursement", "22 April 2024", 800.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(820.0, "Repayment", "25 April 2024", 0.0, 800.0, 0.0, 0.0, 20.0, 0.0, 0.0), + transaction(20.0, "Accrual", "25 April 2024", 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0)); + }); + } +} 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 75c1656ac..67ec2d799 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 @@ -163,6 +163,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest { protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN); protected GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper(); protected final CodeHelper codeHelper = new CodeHelper(); + protected final ChargesHelper chargesHelper = new ChargesHelper(); protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue, double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {