FINERACT-2060: Progressive loan - final accrual calculation on closed loan

This commit is contained in:
Marta Jankovics
2024-12-13 16:48:07 +01:00
committed by Arnold Gálovics
parent c4cdd6f51a
commit 136bb0009a
34 changed files with 682 additions and 837 deletions
@@ -293,10 +293,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
setLoanDelinquencyTag(loan, transactionDate);
@@ -306,6 +304,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
// disable all active standing orders linked to this loan if status
// changes to closed
disableStandingInstructionsLinkedToClosedLoan(loan);
@@ -492,13 +494,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return newPaymentTransaction;
}
@@ -726,13 +728,15 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return newCreditBalanceRefundTransaction;
}
@@ -778,13 +782,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
builderResult.withEntityId(newRefundTransaction.getId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
.withGroupId(loan.getGroupId());
@@ -46,7 +46,7 @@ public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
try {
loanAccrualsProcessingService.addIncomeAndAccrualTransactions(loanId);
loanAccrualsProcessingService.addIncomePostingAndAccruals(loanId);
} catch (Exception e) {
log.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(e);
@@ -466,13 +466,13 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// update the loan object
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, true);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId)
.withLoanId(loanRescheduleRequest.getLoan().getId()).with(changes).withClientId(loan.getClientId())
.withOfficeId(loan.getOfficeId()).withGroupId(loan.getGroupId()).build();
@@ -18,24 +18,29 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -46,131 +51,179 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActivityProcessingService {
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanWritePlatformService loanWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanChargeValidator loanChargeValidator;
private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccountDomainService loanAccountDomainService;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void makeAccrualActivityTransaction(Long loanId, final LocalDate currentDate) {
public void makeAccrualActivityTransaction(@NotNull Long loanId, @NotNull LocalDate currentDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
makeAccrualActivityTransaction(loan, currentDate);
}
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LocalDate currentDate) {
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
// check if loan has installment due on business day
Optional<LoanRepaymentScheduleInstallment> first = loan.getRepaymentScheduleInstallments().stream()
.filter(loanRepaymentScheduleInstallment -> loanRepaymentScheduleInstallment.getDueDate().isEqual(currentDate))
.findFirst();
if (first.isPresent()) {
final LoanRepaymentScheduleInstallment installment = first.get();
// check if there is no not-replayed-accrual-activity related to business date
List<LoanTransaction> loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed()
&& loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate));
if (loanTransactions.isEmpty()) {
loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate);
}
public void makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LocalDate currentDate) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
return;
}
// check if loan has installment in the past or due on current date
List<LoanRepaymentScheduleInstallment> installments = loan
.getRepaymentScheduleInstallments(i -> !i.isDownPayment() && !DateUtils.isBefore(currentDate, i.getDueDate()));
for (LoanRepaymentScheduleInstallment installment : installments) {
LocalDate dueDate = installment.getDueDate();
// check if there is any not-replayed-accrual-activity related to business date
ArrayList<LoanTransaction> existingActivities = new ArrayList<>(
loan.getLoanTransactions(t -> t.isNotReversed() && t.isAccrualActivity() && t.getTransactionDate().isEqual(dueDate)));
boolean hasExisting = !existingActivities.isEmpty();
LoanTransaction existingActivity = hasExisting ? existingActivities.get(0) : null;
makeOrReplayActivity(loan, installment, existingActivity);
if (hasExisting) {
existingActivities.remove(existingActivity);
existingActivities.forEach(this::reverseAccrualActivityTransaction);
}
}
return loan;
}
@Override
@Transactional
public void processAccrualActivityForLoanClosure(Loan loan) {
public void processAccrualActivityForLoanClosure(@NotNull Loan loan) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
return;
}
LocalDate date = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
.filter(LoanTransaction::isAccrualActivity).filter(loanTransaction -> loanTransaction.getDateOf().isAfter(date)).toList();
if (!accrualActivityTransaction.isEmpty()) {
accrualActivityTransaction.forEach(this::reverseAccrualActivityTransaction);
}
LoanTransaction loanTransaction = assembleClosingAccrualActivityTransaction(loan, date);
if (loanTransaction.getAmount().compareTo(BigDecimal.ZERO) != 0) {
loanWritePlatformService.makeAccrualActivityTransaction(loan, loanTransaction);
}
}
// reverse after closure activities
loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(date))
.forEach(this::reverseAccrualActivityTransaction);
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
loanTransaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
private LoanTransaction assembleClosingAccrualActivityTransaction(Loan loan, LocalDate date) {
// collect fees
// calculate activity amounts
BigDecimal feeChargesPortion = BigDecimal.ZERO;
// collect penalties
BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
// collect interests
BigDecimal interestPortion = BigDecimal.ZERO;
var currency = loan.getCurrency();
// sum up all accruals
// collect installment amounts
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
feeChargesPortion = installment.getFeeAccrued(currency).getAmount().add(feeChargesPortion);
penaltyChargesPortion = installment.getPenaltyAccrued(currency).getAmount().add(penaltyChargesPortion);
interestPortion = installment.getInterestAccrued(currency).getAmount().add(interestPortion);
feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged());
penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges());
interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged());
}
List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream().filter(LoanTransaction::isAccrualActivity)
.filter(LoanTransaction::isNotReversed).toList();
// subtract already Posted accruals
// subtract already posted activities
for (LoanTransaction accrualActivity : accrualActivities) {
if (accrualActivity.getFeeChargesPortion() != null) {
feeChargesPortion = feeChargesPortion.subtract(accrualActivity.getFeeChargesPortion());
}
if (accrualActivity.getPenaltyChargesPortion() != null) {
penaltyChargesPortion = penaltyChargesPortion.subtract(accrualActivity.getPenaltyChargesPortion());
}
if (accrualActivity.getInterestPortion() != null) {
interestPortion = interestPortion.subtract(accrualActivity.getInterestPortion());
}
feeChargesPortion = MathUtil.subtract(feeChargesPortion, accrualActivity.getFeeChargesPortion());
penaltyChargesPortion = MathUtil.subtract(penaltyChargesPortion, accrualActivity.getPenaltyChargesPortion());
interestPortion = MathUtil.subtract(interestPortion, accrualActivity.getInterestPortion());
}
BigDecimal transactionAmount = feeChargesPortion.add(penaltyChargesPortion).add(interestPortion);
ExternalId externalId = externalIdFactory.create();
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date, transactionAmount, null,
interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion);
if (!MathUtil.isGreaterThanZero(transactionAmount)) {
return;
}
if (MathUtil.isLessThanZero(feeChargesPortion) || MathUtil.isLessThanZero(penaltyChargesPortion)
|| MathUtil.isLessThanZero(interestPortion)) {
// TODO reverse latest accrual activity if any amount is negative
return;
}
LoanTransaction newActivity = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null,
externalIdFactory.create());
makeAccrualActivityTransaction(loan, newActivity);
}
@Override
@Transactional
public void processAccrualActivityForLoanReopen(Loan loan) {
LoanTransaction lastAccrualActivityMarkedToReverse = null;
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream()
.filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity())
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
public void processAccrualActivityForLoanReopen(@NotNull Loan loan) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
return;
}
// grab the latest AccrualActivityTransaction
// it does not matter if it is on an installment due date or not because it was posted due to loan close
if (!accrualActivityTransaction.isEmpty()) {
lastAccrualActivityMarkedToReverse = accrualActivityTransaction.get(accrualActivityTransaction.size() - 1);
}
LoanTransaction lastAccrualActivityMarkedToReverse = loan.getLoanTransactions().stream()
.filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity())
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).reduce((first, second) -> second).orElse(null);
final LocalDate lastAccrualActivityTransactionDate = lastAccrualActivityMarkedToReverse == null ? null
: lastAccrualActivityMarkedToReverse.getDateOf();
LocalDate today = DateUtils.getBusinessLocalDate();
final List<LoanRepaymentScheduleInstallment> installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate = loan
.getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(today) && (lastAccrualActivityTransactionDate == null
|| installment.getDueDate().isAfter(lastAccrualActivityTransactionDate)
// if close event happened on installment due date
// we should reverse replay it to calculate installment related accrual parts only
|| installment.getDueDate().isEqual(lastAccrualActivityTransactionDate)))
.filter(installment -> installment.getDueDate().isBefore(today)
&& (DateUtils.isAfter(installment.getDueDate(), lastAccrualActivityTransactionDate)
// if close event happened on installment due date
// we should reverse replay it to calculate installment related accrual parts only
|| installment.getDueDate().isEqual(lastAccrualActivityTransactionDate)))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList();
for (LoanRepaymentScheduleInstallment installment : installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate) {
if (lastAccrualActivityMarkedToReverse != null) {
loanWritePlatformService.reverseReplayAccrualActivityTransaction(loan, lastAccrualActivityMarkedToReverse, installment,
installment.getDueDate());
lastAccrualActivityMarkedToReverse = null;
} else {
loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, installment.getDueDate());
}
makeOrReplayActivity(loan, installment, lastAccrualActivityMarkedToReverse);
lastAccrualActivityMarkedToReverse = null;
}
if (lastAccrualActivityMarkedToReverse != null) {
reverseAccrualActivityTransaction(lastAccrualActivityMarkedToReverse);
}
}
private void makeOrReplayActivity(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
LoanTransaction existingActivity) {
LocalDate dueDate = installment.getDueDate();
if (existingActivity == null) {
makeAccrualActivityTransaction(loan, installment, dueDate);
} else {
reverseReplayAccrualActivityTransaction(loan, existingActivity, installment, dueDate);
}
}
private LoanTransaction reverseReplayAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanTransaction loanTransaction,
@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate transactionDate) {
if (validateActivityTransaction(installment, loanTransaction)) {
return loanTransaction;
}
LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (newLoanTransaction != null) {
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
reverseAccrualActivityTransaction(loanTransaction);
return newLoanTransaction;
}
private boolean validateActivityTransaction(@NotNull LoanRepaymentScheduleInstallment installment,
@NotNull LoanTransaction transaction) {
return DateUtils.isEqual(installment.getDueDate(), transaction.getDateOf())
&& MathUtil.isEqualTo(transaction.getInterestPortion(), installment.getInterestCharged())
&& MathUtil.isEqualTo(transaction.getFeeChargesPortion(), installment.getFeeChargesCharged())
&& MathUtil.isEqualTo(transaction.getPenaltyChargesPortion(), installment.getPenaltyCharges());
}
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
private LoanTransaction makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
@NotNull LocalDate transactionDate) {
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
return newAccrualActivityTransaction == null ? null : makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}
private LoanTransaction makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
newAccrualActivityTransaction = loanAccountDomainService
.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionAccrualActivityPostBusinessEvent(newAccrualActivityTransaction));
return newAccrualActivityTransaction;
}
}
@@ -0,0 +1,72 @@
/**
* 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.portfolio.loanaccount.service;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent;
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;
@Slf4j
@RequiredArgsConstructor
public class LoanAccrualEventService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
}
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
@Override
public void onBusinessEvent(LoanCloseBusinessEvent event) {
final Loan loan = event.get();
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan closure on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
}
private final class LoanBalanceChangedListener implements BusinessEventListener<LoanBalanceChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanBalanceChangedBusinessEvent event) {
final Loan loan = event.get();
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan balance change on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
}
}
@@ -56,14 +56,12 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
@@ -77,8 +75,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.stereotype.Component;
@@ -102,9 +98,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
private final LoanTransactionRepository loanTransactionRepository;
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final OfficeRepository officeRepository;
private final LoanChargeRepository loanChargeRepository;
private final LoanChargeValidator loanChargeValidator;
/**
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
@@ -167,9 +160,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
*/
@Override
public void reprocessExistingAccruals(@NotNull Loan loan) {
if (isProgressiveAccrual(loan)) {
return;
}
List<LoanTransaction> accrualTransactions = retrieveListOfAccrualTransactions(loan);
if (!accrualTransactions.isEmpty()) {
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
@@ -185,7 +175,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
*/
@Override
@Transactional
public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) {
public void processAccrualsOnInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled, boolean addJournal) {
if (isProgressiveAccrual(loan)) {
return;
}
@@ -194,13 +184,31 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return;
}
try {
addPeriodicAccruals(accruedTill, loan);
addAccruals(loan, accruedTill, true, false, addJournal);
} catch (Exception e) {
String globalisationMessageCode = "error.msg.accrual.exception";
throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e);
}
}
@Transactional
@Override
public void addIncomePostingAndAccruals(Long loanId) throws LoanNotFoundException {
if (loanId == null) {
return;
}
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (isProgressiveAccrual(loan)) {
return;
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
processIncomePostingAndAccruals(loan);
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
/**
* method calculates accruals for loan with interest recalculation and compounding to be posted as income
*/
@@ -228,26 +236,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
}
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments);
reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate());
reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate());
}
@Transactional
@Override
public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException {
if (loanId == null) {
return;
}
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (isProgressiveAccrual(loan)) {
return;
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
processIncomePostingAndAccruals(loan);
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate(), false);
reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate(), false);
}
/**
@@ -264,32 +254,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
processIncomeAndAccrualTransactionOnLoanClosure(loan);
}
@Override
public void processAccrualsOnLoanReopen(@NotNull Loan loan) {
if (isProgressiveAccrual(loan)) {
List<LoanTransaction> accruals = retrieveListOfAccrualTransactions(loan);
LocalDate accruedTill = loan.getAccruedTill();
// if accruedTill is null, the all the accrual transactions will be reversed here
if (reverseTransactionsAfter(accruals, accruedTill)) {
return;
}
LoanTransaction closureAccrual = accruals.stream()
.filter(t -> !t.isReversed() && t.isAccrual() && DateUtils.isEqual(t.getTransactionDate(), accruedTill))
.sorted(LoanTransactionComparator.INSTANCE).reduce((first, second) -> second).orElse(null);
if (closureAccrual == null) {
return;
}
int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment(
i -> isInPeriod(closureAccrual.getDateOf(), i, i.getInstallmentNumber().equals(firstInstallmentNumber)));
if (MathUtil.isGreaterThan(closureAccrual.getInterestPortion(), installment.getInterestCharged())
|| MathUtil.isGreaterThan(closureAccrual.getFeeChargesPortion(), installment.getFeeChargesCharged())
|| MathUtil.isGreaterThan(closureAccrual.getPenaltyChargesPortion(), installment.getPenaltyCharges())) {
reverseAccrual(closureAccrual);
}
}
}
/**
* method calculates accruals for loan on loan fore closure
*/
@@ -301,7 +265,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) {
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
MonetaryCurrency currency = loan.getCurrency();
reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate);
reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false);
HashMap<String, Object> incomeDetails = new HashMap<>();
@@ -331,20 +295,29 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return;
}
List<LoanTransaction> existingAccruals = retrieveListOfAccrualTransactions(loan);
reverseTransactionsAfter(existingAccruals, loan.getLastLoanRepaymentScheduleInstallment().getDueDate());
LocalDate lastDueDate = loan.getLastLoanRepaymentScheduleInstallment().getDueDate();
reverseTransactionsAfter(existingAccruals, lastDueDate, addJournal);
ensureAccrualTransactionMappings(loan);
if (DateUtils.isAfter(tillDate, lastDueDate)) {
tillDate = lastDueDate;
}
boolean progressiveAccrual = isProgressiveAccrual(loan);
LocalDate accruedTill = loan.getAccruedTill();
if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill)
&& existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), tillDate))) {
return;
LocalDate businessDate = DateUtils.getBusinessLocalDate();
LocalDate accrualDate = isFinal
? (progressiveAccrual ? (DateUtils.isBefore(lastDueDate, businessDate) ? lastDueDate : businessDate)
: getFinalAccrualTransactionDate(loan))
: tillDate;
if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill)) {
if (isFinal) {
reverseTransactionsAfter(existingAccruals, accrualDate, addJournal);
} else if (existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), accrualDate))) {
return;
}
}
AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic);
LocalDate accrualDate = isFinal ? (progressiveAccrual ? DateUtils.getBusinessLocalDate() : getFinalAccrualTransactionDate(loan))
: tillDate;
boolean mergeTransactions = isFinal || progressiveAccrual;
MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency();
List<LoanTransaction> accrualTransactions = new ArrayList<>();
@@ -399,17 +372,17 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return;
}
if (!isFinal) {
loan.setAccruedTill(tillDate);
if (!isFinal || progressiveAccrual) {
loan.setAccruedTill(isFinal ? accrualDate : tillDate);
}
ArrayList<Map<String, Object>> newTransactionMapping = new ArrayList<>();
for (LoanTransaction accrualTransaction : accrualTransactions) {
accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction);
LoanTransactionBusinessEvent businessEvent = accrualTransaction.isAccrual()
? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
if (addJournal) {
LoanTransactionBusinessEvent businessEvent = accrualTransaction.isAccrual()
? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode()));
}
}
@@ -584,7 +557,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency);
Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived);
chargeData.setChargeAccrued(MathUtil.minusToZero(MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency), transactionWaived));
Money transactionAccrued = MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency);
chargeData.setTransactionAccrued(transactionAccrued);
chargeData.setChargeAccrued(MathUtil.minusToZero(transactionAccrued, transactionWaived));
duePeriod.addCharge(chargeData);
}
@@ -720,17 +695,29 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return;
}
ensureAccrualTransactionMappings(loan);
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate();
for (LoanRepaymentScheduleInstallment installment : installments) {
checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment);
LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment();
LocalDate lastDueDate = lastInstallment.getDueDate();
if (isProgressiveAccrual(loan)) {
AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, lastDueDate, true);
for (AccrualPeriodData period : accrualPeriods.getPeriods()) {
Money interestAccrued = period.getTransactionAccrued();
Money feeAccrued = period.getFeeTransactionAccrued();
Money penaltyAccrued = period.getPenaltyTransactionAccrued();
LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(period.getInstallmentNumber());
installment.updateAccrualPortion(interestAccrued, feeAccrued, penaltyAccrued);
}
} else {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate();
for (LoanRepaymentScheduleInstallment installment : installments) {
checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment);
}
}
// reverse accruals after last installment
LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment();
reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate());
reverseTransactionsAfter(accrualTransactions, lastDueDate, false);
}
private void reprocessNonPeriodicAccruals(Loan loan, final List<LoanTransaction> accruals) {
private void reprocessNonPeriodicAccruals(Loan loan, final List<LoanTransaction> accrualTransactions) {
if (isProgressiveAccrual(loan)) {
return;
}
@@ -738,29 +725,25 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
ExternalId externalId = ExternalId.empty();
boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled();
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) {
if (loanTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
"reversed");
loanTransaction.reverse();
for (LoanTransaction accrualTransaction : accrualTransactions) {
if (accrualTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) {
if (accrualTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) {
accrualTransaction.reverse();
if (isExternalIdAutoGenerationEnabled) {
externalId = ExternalId.generate();
}
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan,
final LoanTransaction interestAccrualTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan,
interestApplied, loan.getDisbursementDate(), externalId);
loan.addLoanTransaction(interestAppliedTransaction);
loan.addLoanTransaction(interestAccrualTransaction);
}
} else {
Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid();
Set<LoanChargePaidBy> chargePaidBies = accrualTransaction.getLoanChargesPaid();
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
Money chargeAmount = loanCharge.getAmount(loan.getCurrency());
if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(loan.getCurrency()))) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
loanTransaction, "reversed");
loanTransaction.reverse();
loan.handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate());
if (chargeAmount.isNotEqualTo(accrualTransaction.getAmount(loan.getCurrency()))) {
accrualTransaction.reverse();
loan.handleChargeAppliedTransaction(loanCharge, accrualTransaction.getTransactionDate());
}
}
}
@@ -786,8 +769,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
interest = interest.minus(accrualTransaction.getInterestPortion(currency));
fee = fee.minus(accrualTransaction.getFeeChargesPortion(currency));
penalty = penalty.minus(accrualTransaction.getPenaltyChargesPortion(currency));
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(accrualTransaction.getLoan(),
accrualTransaction, "reversed");
accrualTransaction.reverse();
}
}
@@ -869,8 +850,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
loan.addLoanTransaction(transaction);
} else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingIncomeTransaction.getLoan(),
existingIncomeTransaction, "reversed");
existingIncomeTransaction.reverse();
LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
@@ -889,8 +868,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
if (existingAccrualTransaction == null
|| !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) {
if (existingAccrualTransaction != null) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingAccrualTransaction.getLoan(),
existingAccrualTransaction, "reversed");
existingAccrualTransaction.reverse();
}
LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
@@ -910,8 +887,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
&& loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) {
LocalDate closedDate = loan.getClosedOnDate();
reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate);
reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate);
reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate, false);
reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate, false);
HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments);
@@ -1188,36 +1165,34 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
}
}
private boolean reverseTransactionsAfter(List<LoanTransaction> transactions, LocalDate effectiveDate) {
private boolean reverseTransactionsAfter(List<LoanTransaction> accrualTransactions, LocalDate effectiveDate, boolean addEvent) {
boolean reversed = false;
for (LoanTransaction loanTransaction : transactions) {
if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
"reversed");
reverseAccrual(loanTransaction);
for (LoanTransaction accrualTransaction : accrualTransactions) {
if (!accrualTransaction.isReversed() && DateUtils.isAfter(accrualTransaction.getTransactionDate(), effectiveDate)) {
reverseAccrual(accrualTransaction, addEvent);
reversed = true;
}
}
return reversed;
}
private boolean reverseTransactionsOnOrAfter(List<LoanTransaction> transactions, LocalDate date) {
private boolean reverseTransactionsOnOrAfter(List<LoanTransaction> accrualTransactions, LocalDate date, boolean addEvent) {
boolean reversed = false;
for (LoanTransaction loanTransaction : transactions) {
if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
"reversed");
reverseAccrual(loanTransaction);
for (LoanTransaction accrualTransaction : accrualTransactions) {
if (!accrualTransaction.isReversed() && !DateUtils.isBefore(accrualTransaction.getTransactionDate(), date)) {
reverseAccrual(accrualTransaction, addEvent);
reversed = true;
}
}
return reversed;
}
private void reverseAccrual(LoanTransaction transaction) {
private void reverseAccrual(LoanTransaction transaction, boolean addEvent) {
transaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
if (addEvent) {
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
}
private LocalDate getFinalAccrualTransactionDate(Loan loan) {
@@ -1229,7 +1204,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
}
public boolean isProgressiveAccrual(@NotNull Loan loan) {
return loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE;
return loan.isProgressiveSchedule();
}
private void setSetHelpers(Loan loan) {
@@ -301,17 +301,18 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withEntityId(loanCharge.getId()) //
.withEntityExternalId(loanCharge.getExternalId()) //
@@ -825,14 +826,14 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
}
@@ -1050,9 +1051,11 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|| (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()
&& DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate()))) {
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null);
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));
if (applyLoanChargeTransaction != null) {
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));
}
}
return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate());
}
@@ -32,7 +32,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct
@@ -42,21 +41,14 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
new LoanAccrualActivityPostingLoanStatusChangedListener());
}
private final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
private static final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get();
log.debug("Loan Status change for loan {}", loan.getId());
LoanStatus oldStatus = event.getOldStatus();
LoanStatus newStatus = loan.getStatus();
if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) {
log.debug("Loan Status {} for loan {}", newStatus.getCode(), loan.getId());
loan.updateLoanSummaryDerivedFields();
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
} else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
if (loan.isOpen()) {
loan.handleMaturityDateActivate();
loanAccrualsProcessingService.processAccrualsOnLoanReopen(loan);
}
}
}
@@ -70,9 +62,7 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
LoanStatus oldStatus = event.getOldStatus();
LoanStatus newStatus = loan.getStatus();
if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
} else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Map;
@@ -26,7 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -68,16 +69,17 @@ public class LoanTransactionAssembler {
txnExternalId, null);
}
public LoanTransaction assembleAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
MonetaryCurrency currency = loan.getCurrency();
public LoanTransaction assembleAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
@NotNull LocalDate transactionDate) {
ExternalId externalId = externalIdFactory.create();
BigDecimal interestPortion = installment.getInterestCharged(currency).getAmount();
BigDecimal feeChargesPortion = installment.getFeeChargesCharged(currency).getAmount();
BigDecimal penaltyChargesPortion = installment.getPenaltyChargesCharged(currency).getAmount();
BigDecimal transactionAmount = interestPortion.add(feeChargesPortion).add(penaltyChargesPortion);
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), transactionDate,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
BigDecimal interestPortion = installment.getInterestCharged();
BigDecimal feeChargesPortion = installment.getFeeChargesCharged();
BigDecimal penaltyChargesPortion = installment.getPenaltyCharges();
BigDecimal transactionAmount = MathUtil.add(interestPortion, feeChargesPortion, penaltyChargesPortion);
return MathUtil.isGreaterThanZero(transactionAmount)
? new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), transactionDate,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId)
: null;
}
}
@@ -95,8 +95,6 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdra
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
@@ -230,7 +228,6 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final PlatformSecurityContext context;
private final LoanTransactionValidator loanTransactionValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
@@ -499,9 +496,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
createNote(loan, command, changes);
// auto create standing instruction
createStandingInstruction(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
@@ -526,8 +520,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
// Post Dated Checks
@@ -551,6 +545,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
@@ -869,8 +866,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), true);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan));
}
@@ -1085,8 +1082,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan,
LoanTransaction newInterestPaymentWaiverTransaction) {
LocalDate recalculateFrom = null;
LocalDate transactionDate = newInterestPaymentWaiverTransaction.getTransactionDate();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = newInterestPaymentWaiverTransaction.getTransactionDate();
recalculateFrom = transactionDate;
}
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
@@ -1102,14 +1100,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
final LoanRepaymentScheduleInstallment currentInstallment = loan
.fetchLoanRepaymentScheduleInstallmentByDueDate(newInterestPaymentWaiverTransaction.getTransactionDate());
final LoanRepaymentScheduleInstallment currentInstallment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(transactionDate);
boolean reprocess = true;
if (!loan.isForeclosure() && isTransactionChronologicallyLatest
&& DateUtils.isEqualBusinessDate(newInterestPaymentWaiverTransaction.getTransactionDate()) && currentInstallment != null
&& currentInstallment.getTotalOutstanding(loan.getCurrency())
if (!loan.isForeclosure() && isTransactionChronologicallyLatest && DateUtils.isEqualBusinessDate(transactionDate)
&& currentInstallment != null && currentInstallment.getTotalOutstanding(loan.getCurrency())
.isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) {
reprocess = false;
}
@@ -1155,66 +1150,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return loan.reprocessTransactions();
}
@Transactional
@Override
public Loan reverseReplayAccrualActivityTransaction(Loan loan, final LoanTransaction loanTransaction,
final LoanRepaymentScheduleInstallment installment, final LocalDate transactionDate) {
LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf())
|| !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
"reversed");
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
return loan;
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (newAccrualActivityTransaction.getAmount().compareTo(BigDecimal.ZERO) == 0) {
return loan;
}
return makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionAccrualActivityPostBusinessEvent(newAccrualActivityTransaction));
return loan;
}
@Transactional
@Override
public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command);
final Long loanId = command.getLoanId();
@@ -1245,10 +1183,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveNote(noteText, loan, newInterestPaymentWaiverTransaction);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
// disable all active standing orders linked to this loan if status changes to closed
@@ -1264,6 +1200,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withLoanId(loan.getId()) //
.withEntityId(newInterestPaymentWaiverTransaction.getId()) //
@@ -1564,11 +1503,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.updateLoanSummaryAndStatus();
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -1586,6 +1522,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(entityId) //
@@ -1647,9 +1586,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
writeOffTransaction.reverse();
}
if (loan.isClosedObligationsMet() || loan.isClosedWrittenOff() || loan.isClosedWithOutstandingAmountMarkedForReschedule()) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, loan);
}
loan.updateLoanSummaryAndStatus();
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
@@ -1850,14 +1787,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(waiveInterestTransaction.getId()) //
@@ -1918,44 +1856,48 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.WRITE_OFF_OUTSTANDING,
writtenOffOnLocalDate);
final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, writeOff, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
return new CommandProcessingResultBuilder() //
CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(writeOff.getId()) //
.withEntityExternalId(writeOff.getExternalId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes).build();
.with(changes);
final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
if (changedTransactionDetail != null) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, writeOff, noteText);
this.noteRepository.save(note);
}
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
builder.withEntityId(writeOff.getId()).withEntityExternalId(writeOff.getExternalId());
}
return builder.build();
}
@Transactional
@@ -2020,12 +1962,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note);
}
if (possibleClosingTransaction != null) {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -2037,6 +1976,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// disable all active standing instructions linked to the loan
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
CommandProcessingResult result;
if (possibleClosingTransaction != null) {
@@ -2654,15 +2596,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
if (writeOffTransaction != null) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
@@ -2811,11 +2755,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
createLoanScheduleArchive(loan, scheduleGeneratorDTO);
}
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
@@ -2879,11 +2825,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
return loan;
}
@@ -3396,8 +3343,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// Create journal entries for the new transaction(s)
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
// Calculate and apply delinquency tag (if applicable)
loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
// disable all active standing orders linked to this loan if status
@@ -3414,11 +3359,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, refundTransaction);
}
// Raise business events
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
// Create journal entries for the new transaction(s)
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
Long entityId = refundTransaction.getId();
ExternalId entityExternalId = refundTransaction.getExternalId();
Long subEntityId = interestRefundTransaction != null ? interestRefundTransaction.getId() : null;
@@ -100,6 +100,7 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlat
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
@@ -332,9 +333,16 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanStatusChangePlatformService.class)
public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService,
LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualActivityProcessingService);
}
@Bean
@ConditionalOnMissingBean(LoanAccrualEventService.class)
public LoanAccrualEventService loanAccrualEventService(BusinessEventNotifierService businessEventNotifierService,
LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService,
return new LoanAccrualEventService(businessEventNotifierService, loanAccrualsProcessingService,
loanAccrualActivityProcessingService);
}
@@ -351,8 +359,8 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory,
PlatformSecurityContext context, LoanTransactionValidator loanTransactionValidator,
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
LoanTransactionValidator loanTransactionValidator,
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
@@ -381,18 +389,17 @@ public class LoanAccountConfiguration {
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator,
loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository,
loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService,
calendarInstanceRepository, paymentDetailWritePlatformService, holidayRepository, configurationDomainService,
workingDaysRepository, accountTransfersWritePlatformService, accountTransfersReadPlatformService,
accountAssociationsReadPlatformService, loanReadPlatformService, fromApiJsonHelper, calendarRepository,
loanScheduleHistoryWritePlatformService, loanApplicationValidator, accountAssociationRepository,
accountTransferDetailRepository, businessEventNotifierService, guarantorDomainService, loanUtilService, loanSummaryWrapper,
entityDatatableChecksWritePlatformService, transactionProcessingStrategy, codeValueRepository,
cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler,
postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine,
loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository,
accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountAssociationsReadPlatformService,
loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanScheduleHistoryWritePlatformService,
loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService,
guarantorDomainService, loanUtilService, loanSummaryWrapper, entityDatatableChecksWritePlatformService,
transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository,
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService);
@@ -128,6 +128,9 @@ class LoanChargeWritePlatformServiceImplTest {
@Mock
private LoanChargeValidator loanChargeValidator;
@Mock
private LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
@BeforeEach
void setUp() {
when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
@@ -240,8 +240,6 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
when(loan.isClosedWrittenOff()).thenReturn(false);
when(loan.isClosedObligationsMet()).thenReturn(false);
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act
@@ -287,8 +285,6 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
when(loan.isClosedWrittenOff()).thenReturn(false);
when(loan.isClosedObligationsMet()).thenReturn(false);
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act