From 093054b3dbaf90b5dc66cc3d2b929101815902b9 Mon Sep 17 00:00:00 2001 From: Andrii Kulminskyi Date: Mon, 20 Jan 2025 14:02:38 +0200 Subject: [PATCH] FINERACT-1981: Consolidated accrual activity created instead of individual ones --- ...nAccrualActivityProcessingServiceImpl.java | 74 ++++++++---- ...TransactionAccrualActivityPostingTest.java | 11 +- .../MultiActivityAccrualsTest.java | 107 ++++++++++++++++++ 3 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/MultiActivityAccrualsTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java index f644b904821..4da472e3893 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -92,42 +92,66 @@ public void processAccrualActivityForLoanClosure(@NotNull Loan loan) { if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { return; } - LocalDate transactionDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); - // reverse after closure activities - loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(transactionDate)) + + LocalDate closureDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); + + // Reverse accrual activities posted after the closure date + loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(closureDate)) .forEach(this::reverseAccrualActivityTransaction); - // calculate activity amounts BigDecimal feeChargesPortion = BigDecimal.ZERO; BigDecimal penaltyChargesPortion = BigDecimal.ZERO; BigDecimal interestPortion = BigDecimal.ZERO; - // collect installment amounts + + // Calculate total portions from all installments for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { - feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged()); - penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges()); - interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged()); + if (!installment.isDownPayment()) { // Exclude downpayment installments + feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged()); + penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges()); + interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged()); + } } + List accrualActivities = loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed()); - // subtract already posted activities - for (LoanTransaction accrualActivity : accrualActivities) { - if (MathUtil.isLessThan(feeChargesPortion, accrualActivity.getFeeChargesPortion()) - || MathUtil.isLessThan(penaltyChargesPortion, accrualActivity.getPenaltyChargesPortion()) - || MathUtil.isLessThan(interestPortion, accrualActivity.getInterestPortion())) { - reverseAccrualActivityTransaction(accrualActivity); - } else { - feeChargesPortion = MathUtil.subtract(feeChargesPortion, accrualActivity.getFeeChargesPortion()); - penaltyChargesPortion = MathUtil.subtract(penaltyChargesPortion, accrualActivity.getPenaltyChargesPortion()); - interestPortion = MathUtil.subtract(interestPortion, accrualActivity.getInterestPortion()); + + // Check each past installment for accrual activity + for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + if (!installment.isDownPayment() && !installment.isAdditional() && installment.getDueDate().isBefore(closureDate)) { + List installmentAccruals = accrualActivities.stream() + .filter(t -> t.getDateOf().isEqual(installment.getDueDate())).toList(); + + if (installmentAccruals.isEmpty()) { + // No AAT for this installment; create one + makeAccrualActivityTransaction(loan, installment, installment.getDueDate()); + + // Subtract processed portions + } else if (installmentAccruals.size() > 1) { + // Reverse and recreate if inconsistent or duplicate + installmentAccruals.forEach(this::reverseAccrualActivityTransaction); + makeAccrualActivityTransaction(loan, installment, installment.getDueDate()); + } else if (!validateActivityTransaction(installment, installmentAccruals.get(0))) { + reverseReplayAccrualActivityTransaction(loan, installmentAccruals.get(0), installment, installment.getDueDate()); + } } } - BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion); - if (!MathUtil.isGreaterThanZero(transactionAmount)) { - return; + + // Subtract already posted accrual activities + accrualActivities = loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed()); + for (LoanTransaction accrualActivity : accrualActivities) { + feeChargesPortion = MathUtil.subtract(feeChargesPortion, accrualActivity.getFeeChargesPortion()); + penaltyChargesPortion = MathUtil.subtract(penaltyChargesPortion, accrualActivity.getPenaltyChargesPortion()); + interestPortion = MathUtil.subtract(interestPortion, accrualActivity.getInterestPortion()); + } + + // Skip final accrual activity creation if no portions remain + if (MathUtil.isGreaterThanZero(feeChargesPortion) || MathUtil.isGreaterThanZero(penaltyChargesPortion) + || MathUtil.isGreaterThanZero(interestPortion)) { + BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion); + LoanTransaction newActivity = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), + closureDate, transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, + externalIdFactory.create()); + makeAccrualActivityTransaction(loan, newActivity); } - LoanTransaction newActivity = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), - transactionDate, transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, - externalIdFactory.create()); - makeAccrualActivityTransaction(loan, newActivity); } @Override diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 66301d4ac55..e5c9e25d8e0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -876,17 +876,16 @@ public void testAccrualActivityPostingForMultiDisburseLoan() { chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate); addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1); verifyTransactions(loanId.get(), - transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), // - transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // - transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // - transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) // - ); + transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), + transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), + transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), + transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false)); }); runAt(repaymentPeriod1CloseDate, () -> { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get())); verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), // - transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // + transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) // ); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/MultiActivityAccrualsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MultiActivityAccrualsTest.java new file mode 100644 index 00000000000..954a1e054fb --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MultiActivityAccrualsTest.java @@ -0,0 +1,107 @@ +/** + * 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.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.util.List; +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.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +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; + +public class MultiActivityAccrualsTest extends BaseLoanIntegrationTest { + + private static final String disbursementDate = "9 August 2024"; + private static final String repaymentDate = "9 December 2024"; + private static final Double fullRepaymentAmount = 700.0; + private static final Integer expectedNumberOfAccruals = 1; + private static final Integer expectedNumberOfActivityAccruals = 4; + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private ClientHelper clientHelper; + private LoanTransactionHelper loanTransactionHelper; + private static Long loanId; + + @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(create4IProgressive().currencyCode("USD").enableAccrualActivityPosting(true)); + + loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), disbursementDate, 600.0, 9.99, 4, null); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(600), disbursementDate); + } + + @Test + public void testMultiAccrualActivityCreated() { + runAt(repaymentDate, () -> { + + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(repaymentDate).locale("en") + .transactionAmount(fullRepaymentAmount)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + List accrualTransactional = loanDetails.getTransactions().stream() + .filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.accrual")).toList(); + + List accrualActivityTransactional = loanDetails.getTransactions().stream() + .filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.accrualActivity")).toList(); + + assertFalse(accrualTransactional.isEmpty()); + assertEquals(expectedNumberOfAccruals, accrualTransactional.size()); + assertFalse(accrualActivityTransactional.isEmpty()); + assertEquals(expectedNumberOfActivityAccruals, accrualActivityTransactional.size()); + + verifyTransactions(loanId, // + transaction(600.0, "Disbursement", "09 August 2024", 600.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(20.00, "Accrual", "09 December 2024", 0, 0, 20.00, 0, 0, 0.0, 0.0), + transaction(5.0, "Accrual Activity", "09 September 2024", 0, 0, 5.0, 0, 0, 0.0, 0.0), + transaction(5.0, "Accrual Activity", "09 October 2024", 0, 0, 5.0, 0, 0, 0.0, 0.0), + transaction(5.0, "Accrual Activity", "09 November 2024", 0, 0, 5.0, 0, 0, 0.0, 0.0), + transaction(5.0, "Accrual Activity", "09 December 2024", 0, 0, 5.0, 0, 0, 0.0, 0.0), + transaction(700.00, "Repayment", "09 December 2024", 0, 600.00, 20.0, 0, 0, 0.0, 80.0)); + }); + } +}