Skip to content

Commit

Permalink
FINERACT-2081: Accrual not created for closed or overpaid loans
Browse files Browse the repository at this point in the history
  • Loading branch information
kulminsky committed Feb 7, 2025
1 parent 486ecb1 commit 05a885b
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ private void addAccruals(@NotNull Loan loan, @NotNull LocalDate tillDate, boolea
}
}

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<>();
Expand Down Expand Up @@ -393,7 +393,7 @@ private void addAccruals(@NotNull Loan loan, @NotNull LocalDate tillDate, boolea
}
}

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();
Expand All @@ -406,7 +406,7 @@ private AccrualPeriodsData calculateAccrualAmounts(@NotNull Loan loan, @NotNull
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;
}
Expand Down Expand Up @@ -512,12 +512,17 @@ private BigDecimal calcInterestAccruedAmount(@NotNull LoanRepaymentScheduleInsta
}

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;
if (isFinal) {
loanCharges = loan.getLoanCharges();
} else {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
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.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
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 final Integer expectedNumberOfAccruals = 1;

private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
private ClientHelper clientHelper;
private LoanTransactionHelper loanTransactionHelper;
private static Long loanId;
private static Integer penalty;
private static String penaltyCharge1AddedDate;

@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);

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);

penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true));

LocalDate targetDate = LocalDate.of(2024, 4, 24);
penaltyCharge1AddedDate = dateFormatter.format(targetDate);
}

@Test
public void testAccrualCreatedOnLoanClosureWithSubmittedDate() {
runAt(startDate, () -> {
globalConfigurationHelper.updateGlobalConfiguration(CHARGE_ACCRUAL_DATE,
new PutGlobalConfigurationsRequest().stringValue("submitted-date"));

loanTransactionHelper.addLoanCharge(loanId, new PostLoansLoanIdChargesRequest().dateFormat("dd MMMM yyyy").locale("en")
.chargeId((long) penalty).amount(chargeAmount).dueDate(penaltyCharge1AddedDate));

loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
.transactionDate(repaymentDate).locale("en").transactionAmount(repaymentAmount));

logLoanTransactions(loanId);

GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);

logInstallmentsOfLoanDetails(loanDetails);

List<GetLoansLoanIdTransactions> accrualTransactions = loanDetails.getTransactions().stream()
.filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.accrual")).toList();

assertFalse(accrualTransactions.isEmpty(), "Expected accrual transaction on loan closure");
assertEquals(expectedNumberOfAccruals, accrualTransactions.size(), "Incorrect number of accruals");
});
}

private void logLoanTransactions(Long loanId) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
if (loanDetails.getTransactions() != null) {
loanDetails.getTransactions()
.forEach(tr -> log.info("Transaction {} {} {} ", tr.getType().getValue(), tr.getDate(), tr.getAmount()));
}
}

private void logInstallmentsOfLoanDetails(GetLoansLoanIdResponse loanDetails) {
log.info("index, dueDate, principal, fee, penalty, interest");
if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) {
loanDetails.getRepaymentSchedule().getPeriods()
.forEach(period -> log.info("{}, \"{}\", {}, {}, {}, {}", period.getPeriod(),
DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH)
.format(Objects.requireNonNull(period.getDueDate())),
period.getPrincipalDue(), period.getFeeChargesDue(), period.getPenaltyChargesDue(), period.getInterestDue()));
}
}
}

0 comments on commit 05a885b

Please sign in to comment.