FINERACT-2181: Rework "createMissingAccrualTransactionDuringChargeOffIfNeeded" to avoid flushing and triggering business event as part of Transaction processor

This commit is contained in:
mariiaKraievska
2025-03-18 12:03:27 +01:00
committed by Adam Saghy
parent 166c1981fb
commit 22282ded00
16 changed files with 286 additions and 127 deletions
@@ -0,0 +1,36 @@
/**
* 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.data;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
/**
* Represents a transaction change, storing both the old reversed transaction (if any) and the new transaction.
*/
@Getter
@Setter
@AllArgsConstructor
public class TransactionChangeData {
private LoanTransaction oldTransaction;
private LoanTransaction newTransaction;
}
@@ -18,9 +18,12 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
/**
* Stores details of {@link LoanTransaction}'s that were reversed or newly created
@@ -28,8 +31,41 @@ import lombok.Getter;
@Getter
public class ChangedTransactionDetail {
private final Map<Long, LoanTransaction> newTransactionMappings = new LinkedHashMap<>();
private final List<TransactionChangeData> transactionChanges = new ArrayList<>();
private final Map<LoanTransaction, Long> currentTransactionToOldId = new LinkedHashMap<>();
public void addTransactionChange(final TransactionChangeData transactionChangeData) {
for (TransactionChangeData change : transactionChanges) {
if (transactionChangeData.getOldTransaction() != null && change.getOldTransaction() != null
&& Objects.equals(change.getOldTransaction().getId(), transactionChangeData.getOldTransaction().getId())) {
change.setOldTransaction(transactionChangeData.getOldTransaction());
change.setNewTransaction(transactionChangeData.getNewTransaction());
return;
} else if (transactionChangeData.getOldTransaction() == null && change.getOldTransaction() == null
&& change.getNewTransaction() != null
&& Objects.equals(change.getNewTransaction().getId(), transactionChangeData.getNewTransaction().getId())) {
change.setNewTransaction(transactionChangeData.getNewTransaction());
return;
}
}
transactionChanges.add(transactionChangeData);
}
public void addNewTransactionChangeBeforeExistingOne(final TransactionChangeData newTransactionChange,
final LoanTransaction existingLoanTransaction) {
if (existingLoanTransaction != null) {
final Optional<TransactionChangeData> existingChange = transactionChanges.stream().filter(
change -> change.getNewTransaction() != null && Objects.equals(change.getNewTransaction(), existingLoanTransaction))
.findFirst();
if (existingChange.isPresent()) {
transactionChanges.add(transactionChanges.indexOf(existingChange.get()), newTransactionChange);
return;
}
}
transactionChanges.add(newTransactionChange);
}
public void removeTransactionChange(final LoanTransaction newTransaction) {
transactionChanges.removeIf(change -> change.getNewTransaction().equals(newTransaction));
}
}
@@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -719,10 +720,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
change.getNewTransaction().updateLoan(this);
}
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList();
this.loanTransactions.addAll(newTransactions);
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
}
@@ -2656,14 +2659,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
change.getNewTransaction().updateLoan(this);
}
/*
* Commented since throwing exception if external id present for one of the transactions. for this need to save
* the reversed transactions first and then new transactions.
*/
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList();
this.loanTransactions.addAll(newTransactions);
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
@@ -25,7 +25,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -34,6 +33,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -187,7 +187,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
**/
if (newLoanTransaction.isReversed()) {
loanTransaction.reverse();
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), loanTransaction);
changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, loanTransaction));
} else if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
@@ -264,7 +264,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
@Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) {
public ChangedTransactionDetail processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) {
switch (loanTransaction.getTypeOf()) {
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
@@ -288,6 +288,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
}
return ctx.getChangedTransactionDetail();
}
@Override
@@ -421,10 +422,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
private void reprocessChargebackTransactionRelation(ChangedTransactionDetail changedTransactionDetail,
List<LoanTransaction> transactionsToBeProcessed) {
List<LoanTransaction> mergedTransactionList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
for (Map.Entry<Long, LoanTransaction> entry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
if (entry.getValue().isChargeback()) {
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
LoanTransaction newTransaction = change.getNewTransaction();
LoanTransaction oldTransaction = change.getOldTransaction();
if (newTransaction.isChargeback()) {
for (LoanTransaction loanTransaction : mergedTransactionList) {
if (loanTransaction.isReversed()) {
continue;
@@ -433,8 +436,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
LoanTransactionRelation oldLoanTransactionRelation = null;
for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) {
if (LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType())
&& entry.getKey().equals(transactionRelation.getToTransaction().getId())) {
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, entry.getValue(),
&& oldTransaction != null && oldTransaction.getId() != null
&& oldTransaction.getId().equals(transactionRelation.getToTransaction().getId())) {
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction,
LoanTransactionRelationTypeEnum.CHARGEBACK);
oldLoanTransactionRelation = transactionRelation;
break;
@@ -511,8 +515,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
private List<LoanTransaction> getMergedTransactionList(List<LoanTransaction> transactionList,
ChangedTransactionDetail changedTransactionDetail) {
List<LoanTransaction> mergedList = new ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values());
mergedList.addAll(new ArrayList<>(transactionList));
List<LoanTransaction> mergedList = new ArrayList<>(
changedTransactionDetail.getTransactionChanges().stream().map(TransactionChangeData::getNewTransaction).toList());
mergedList.addAll(transactionList);
return mergedList;
}
@@ -525,7 +530,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// Adding Replayed relation from newly created transaction to reversed transaction
newLoanTransaction.getLoanTransactionRelations().add(
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction));
}
protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
@@ -39,8 +39,10 @@ public interface LoanRepaymentScheduleTransactionProcessor {
/**
* Provides support for processing the latest transaction (which should be the latest transaction) against the loan
* schedule.
*
* @return ChangedTransactionDetail
*/
void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx);
ChangedTransactionDetail processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx);
/**
* Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan
@@ -18,14 +18,15 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
@RequiredArgsConstructor
public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTransactionBusinessEventService {
@@ -35,18 +36,30 @@ public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTran
@Override
public void raiseTransactionReplayedEvents(final ChangedTransactionDetail changedTransactionDetail) {
if (changedTransactionDetail == null || changedTransactionDetail.getNewTransactionMappings().isEmpty()) {
if (changedTransactionDetail == null || changedTransactionDetail.getTransactionChanges().isEmpty()) {
return;
}
// Extra safety net to avoid event leaking
try {
businessEventNotifierService.startExternalEventRecording();
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
LoanTransaction oldTransaction = loanTransactionRepository.findById(mapEntry.getKey())
.orElseThrow(() -> new LoanTransactionNotFoundException(mapEntry.getKey()));
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(oldTransaction);
data.setNewTransactionDetail(mapEntry.getValue());
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
final LoanTransaction newTransaction = change.getNewTransaction();
final LoanTransaction oldTransaction = change.getOldTransaction();
if (oldTransaction != null) {
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(oldTransaction);
data.setNewTransactionDetail(newTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
} else {
if (newTransaction.isAccrual()) {
businessEventNotifierService
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(newTransaction));
} else {
businessEventNotifierService
.notifyPostBusinessEvent(new LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction));
}
}
}
businessEventNotifierService.stopExternalEventRecording();
} catch (Exception e) {
@@ -19,17 +19,23 @@
package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public interface ReprocessLoanTransactionsService {
void reprocessTransactions(Loan loan);
void reprocessTransactions(Loan loan, List<LoanTransaction> loanTransactions);
void reprocessTransactionsWithPostTransactionChecks(Loan loan, LocalDate transactionDate);
void processPostDisbursementTransactions(Loan loan);
void removeLoanCharge(Loan loan, LoanCharge loanCharge);
void processLatestTransaction(LoanTransaction loanTransaction, Loan loan);
}
@@ -19,14 +19,18 @@
package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate;
import java.util.Map;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.interestpauses.service.LoanAccountTransfersService;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.springframework.stereotype.Service;
@Service
@@ -45,6 +49,22 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
}
}
@Override
public void reprocessTransactions(final Loan loan, final List<LoanTransaction> loanTransactions) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
final ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
loan.getDisbursementDate(), loanTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
change.getNewTransaction().updateLoan(loan);
}
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList();
loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields();
handleChangedDetail(changedTransactionDetail);
}
@Override
public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) {
final ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionsWithPostTransactionChecks(transactionDate);
@@ -63,12 +83,30 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
loan.removeLoanCharge(loanCharge).ifPresent(this::handleChangedDetail);
}
@Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) {
final ChangedTransactionDetail changedTransactionDetail = loan.getTransactionProcessor().processLatestTransaction(loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail()));
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields();
handleChangedDetail(changedTransactionDetail);
}
private void handleChangedDetail(final ChangedTransactionDetail changedTransactionDetail) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
loanAccountTransfersService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) {
final LoanTransaction newTransaction = change.getNewTransaction();
final LoanTransaction oldTransaction = change.getOldTransaction();
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newTransaction);
if (oldTransaction != null) {
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction);
}
}
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
}