FINERACT-1981: Consolidated accrual activity created instead of individual ones
This commit is contained in:
committed by
Adam Saghy
parent
de1555f68f
commit
093054b3db
+51
-27
@@ -92,42 +92,66 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
|
||||
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());
|
||||
}
|
||||
List<LoanTransaction> 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());
|
||||
if (!installment.isDownPayment()) { // Exclude downpayment installments
|
||||
feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged());
|
||||
penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges());
|
||||
interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged());
|
||||
}
|
||||
}
|
||||
BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion);
|
||||
if (!MathUtil.isGreaterThanZero(transactionAmount)) {
|
||||
return;
|
||||
|
||||
List<LoanTransaction> accrualActivities = loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed());
|
||||
|
||||
// Check each past installment for accrual activity
|
||||
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
if (!installment.isDownPayment() && !installment.isAdditional() && installment.getDueDate().isBefore(closureDate)) {
|
||||
List<LoanTransaction> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
+5
-6
@@ -876,17 +876,16 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
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) //
|
||||
);
|
||||
|
||||
+107
@@ -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<GetLoansLoanIdTransactions> accrualTransactional = loanDetails.getTransactions().stream()
|
||||
.filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.accrual")).toList();
|
||||
|
||||
List<GetLoansLoanIdTransactions> 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user