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()) {
|
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LocalDate transactionDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
|
|
||||||
// reverse after closure activities
|
LocalDate closureDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
|
||||||
loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(transactionDate))
|
|
||||||
|
// Reverse accrual activities posted after the closure date
|
||||||
|
loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(closureDate))
|
||||||
.forEach(this::reverseAccrualActivityTransaction);
|
.forEach(this::reverseAccrualActivityTransaction);
|
||||||
|
|
||||||
// calculate activity amounts
|
|
||||||
BigDecimal feeChargesPortion = BigDecimal.ZERO;
|
BigDecimal feeChargesPortion = BigDecimal.ZERO;
|
||||||
BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
|
BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
|
||||||
BigDecimal interestPortion = BigDecimal.ZERO;
|
BigDecimal interestPortion = BigDecimal.ZERO;
|
||||||
// collect installment amounts
|
|
||||||
|
// Calculate total portions from all installments
|
||||||
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||||
feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged());
|
if (!installment.isDownPayment()) { // Exclude downpayment installments
|
||||||
penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges());
|
feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged());
|
||||||
interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged());
|
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion);
|
|
||||||
if (!MathUtil.isGreaterThanZero(transactionAmount)) {
|
List<LoanTransaction> accrualActivities = loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed());
|
||||||
return;
|
|
||||||
|
// 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
|
@Override
|
||||||
|
|||||||
+5
-6
@@ -876,17 +876,16 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
|||||||
chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate);
|
chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate);
|
||||||
addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1);
|
addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1);
|
||||||
verifyTransactions(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(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(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(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
runAt(repaymentPeriod1CloseDate, () -> {
|
runAt(repaymentPeriod1CloseDate, () -> {
|
||||||
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
|
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
|
||||||
verifyTransactions(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(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(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(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