FINERACT-2081: Accrual not created for closed or overpaid loans

This commit is contained in:
Andrii Kulminskyi
2025-02-11 16:28:24 +01:00
committed by Adam Saghy
parent 352d282d7c
commit 4a0c52ea8b
3 changed files with 102 additions and 8 deletions
@@ -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<LoanTransaction> 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<LoanRepaymentScheduleInstallment> installments = getInstallmentsToAccrue(loan, interestCalculationTillDate, periodic);
List<LoanRepaymentScheduleInstallment> 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<LoanCharge> loanCharges = loan
.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
: isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod())));
Collection<LoanCharge> 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);
}
@@ -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));
});
}
}
@@ -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) {