FINERACT-2225: Save journal entries for accrual transaction created by charge adjustment

This commit is contained in:
Oleksii Novikov
2025-04-01 14:23:55 +02:00
committed by Adam Saghy
parent 510b4853e7
commit f3606bb2d1
4 changed files with 44 additions and 5 deletions
@@ -2722,3 +2722,28 @@ Feature: LoanCharge
| 01 March 2024 | Accrual | 1.16 | 0.0 | 1.16 | 0.0 | 0.0 | 0.0 | false | false |
| 01 March 2024 | Charge-off | 107.14 | 100.0 | 2.14 | 5.0 | 0.0 | 0.0 | false | false |
| 01 March 2024 | Charge Adjustment | 5.0 | 5.0 | 0.0 | 0.0 | 0.0 | 95.0 | false | false |
@TestRailId:C3571
Scenario: Charge adjustment on account with zero principal balance should not create accrual transactions without journal entries
When Admin sets the business date to "25 March 2025"
When Admin creates a client with random data
And Admin successfully creates a new customised Loan submitted on date: "25 March 2025", with Principal: "800", a loanTermFrequency: 1 months, and numberOfRepayments: 1
And Admin successfully approves the loan on "25 March 2025" with "800" amount and expected disbursement date on "25 March 2025"
When Admin successfully disburse the loan on "25 March 2025" with "800" EUR transaction amount
And Customer makes "AUTOPAY" repayment on "25 March 2025" with 800 EUR transaction amount
And Admin adds an NSF fee because of payment bounce with "25 March 2025" transaction date
When Admin makes a charge adjustment for the last "LOAN_NSF_FEE" type charge which is due on "25 March 2025" with 10 EUR transaction amount and externalId ""
Then Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "25 March 2025" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 10.0 |
| INCOME | 404007 | Fee Income | 10.0 | |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 25 March 2025 | Disbursement | 800.0 | 0.0 | 0.0 | 0.0 | 0.0 | 800.0 |
| 25 March 2025 | Repayment | 800.0 | 790.0 | 0.0 | 0.0 | 10.0 | 10.0 |
| 25 March 2025 | Charge Adjustment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 25 March 2025 | Accrual | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 |
Then Loan Transactions tab has a "ACCRUAL" transaction with date "25 March 2025" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | 10.0 | |
| INCOME | 404007 | Fee Income | | 10.0 |
@@ -24,6 +24,7 @@ import java.util.List;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.springframework.lang.NonNull;
public interface LoanAccrualsProcessingService {
@@ -41,7 +42,7 @@ public interface LoanAccrualsProcessingService {
void processIncomePostingAndAccruals(@NotNull Loan loan);
void processAccrualsOnLoanClosure(@NotNull Loan loan);
void processAccrualsOnLoanClosure(@NonNull Loan loan, boolean addJournal);
void processAccrualsOnLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate,
@NotNull List<LoanTransaction> newAccrualTransactions);
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.annotation.PostConstruct;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
@@ -27,6 +28,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBu
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
@Slf4j
@RequiredArgsConstructor
@@ -50,7 +52,7 @@ public class LoanAccrualEventService {
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan closure on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan, false);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
@@ -64,9 +66,19 @@ public class LoanAccrualEventService {
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan balance change on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
final boolean hasChargeAdjustment = hasChargeAdjustment(loan);
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan, hasChargeAdjustment);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
private boolean hasChargeAdjustment(final Loan loan) {
return loan.getLoanTransactions().stream()
.filter(transaction -> transaction.isChargeAdjustment() && transaction.isNotReversed())
.anyMatch(chargeAdjustment -> chargeAdjustment.getLoanTransactionRelations().stream()
.anyMatch(relation -> relation.getRelationType() == LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT
&& relation.getFromTransaction() != null
&& Objects.equals(relation.getFromTransaction().getId(), chargeAdjustment.getId())));
}
}
}
@@ -83,6 +83,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -284,9 +285,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
* method calculates accruals for loan on loan closure
*/
@Override
public void processAccrualsOnLoanClosure(@NotNull Loan loan) {
public void processAccrualsOnLoanClosure(@NonNull final Loan loan, final boolean addJournal) {
// check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income
addAccruals(loan, loan.getLastLoanRepaymentScheduleInstallment().getDueDate(), false, true, false);
addAccruals(loan, loan.getLastLoanRepaymentScheduleInstallment().getDueDate(), false, true, addJournal);
if (isProgressiveAccrual(loan)) {
return;
}