FINERACT-2148: Accelerate maturity to charge-off date when a regular/backdated repayment or other monetary activity occurs before/after the charge-off date.

This commit is contained in:
mariiaKraievska
2025-01-21 10:44:47 +01:00
committed by Adam Saghy
parent 8ab9c9ea9a
commit 75701daa09
10 changed files with 358 additions and 32 deletions
@@ -99,6 +99,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
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.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
@@ -979,6 +980,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} else {
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.getLoanTransactions().add(refundTransaction);
if (interestRefundTransaction != null) {
@@ -287,6 +287,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
if (reprocessRequired) {
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
ChangedTransactionDetail changedTransactionDetail = overpaidReprocess
? loan.reprocessTransactionsWithPostTransactionChecks(transactionDate)
: loan.reprocessTransactions();
@@ -813,6 +820,12 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (reprocessRequired) {
addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate);
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings()
@@ -853,6 +866,12 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
.determineProcessor(loan.transactionProcessingStrategy());
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
if (loan.isInterestBearing() && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.reprocessTransactions();
} else {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
@@ -1440,6 +1459,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
// Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule
// to be reprocessed.
@@ -1451,6 +1474,11 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
*/
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.reprocessTransactions();
} else {
// reprocess loan schedule based on charge been waived.
@@ -1144,6 +1144,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
} else if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
return loan.reprocessTransactions();
}
@@ -3624,6 +3628,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
}
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
changedTransactionDetail = loan.reprocessTransactions();
LocalDate lastLoanTransactionDate = loan.getLatestTransactionDate();
loan.doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine);
@@ -3738,7 +3747,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule());
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !(loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff))) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
@@ -3809,6 +3820,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
"reversed");
writeOffTransaction.reverse();
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, loan);
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
return loan.reprocessTransactions();
}
@@ -33,6 +33,7 @@ import org.apache.commons.lang3.tuple.Pair;
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.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -41,6 +42,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.context.annotation.Conditional;
@@ -56,6 +58,8 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
private final EMICalculator emiCalculator;
private final LoanAssembler loanAssembler;
private final LoanScheduleService loanScheduleService;
private final LoanUtilService loanUtilService;
private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal,
List<LoanTransaction> collect) {
@@ -81,6 +85,13 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
@@ -83,29 +83,34 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate);
if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess,
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight();
if (!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getCurrentTransactionToOldId().isEmpty()
|| !changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getNewTransactionMappings().isEmpty()) {
List<Long> replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft()
.getNewTransactionMappings().keySet().stream().toList();
log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}",
replayedTransactions);
}
if (model != null) {
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, loanRepaymentScheduleInstallment.getDueDate(),
businessDate);
if (dueAmounts != null) {
BigDecimal interestPaid = loanRepaymentScheduleInstallment.getInterestPaid();
BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount();
if (interestPaid == null) {
return dueInterest;
if (loan.isChargedOff()) {
return loanRepaymentScheduleInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
} else {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess,
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight();
if (!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getCurrentTransactionToOldId().isEmpty()
|| !changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getNewTransactionMappings()
.isEmpty()) {
List<Long> replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft()
.getNewTransactionMappings().keySet().stream().toList();
log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}",
replayedTransactions);
}
if (model != null) {
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, loanRepaymentScheduleInstallment.getDueDate(),
businessDate);
if (dueAmounts != null) {
BigDecimal interestPaid = loanRepaymentScheduleInstallment.getInterestPaid();
BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount();
if (interestPaid == null) {
return dueInterest;
}
return dueInterest.subtract(interestPaid);
}
return dueInterest.subtract(interestPaid);
}
}
}
@@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -51,8 +52,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.springframework.stereotype.Service;
@@ -72,6 +76,8 @@ public class LoanReAgingServiceImpl {
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final NoteRepository noteRepository;
private final LoanChargeValidator loanChargeValidator;
private final LoanUtilService loanUtilService;
private final LoanScheduleService loanScheduleService;
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
@@ -123,6 +129,12 @@ public class LoanReAgingServiceImpl {
}
reverseReAgeTransaction(reAgeTransaction, command);
loanTransactionRepository.saveAndFlush(reAgeTransaction);
if (loan.getLoanProductRelatedDetail() != null
&& loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)
&& loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff)) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.reprocessTransactions();
loan.updateLoanScheduleDependentDerivedFields();
persistNote(loan, command, changes);