FINERACT-2354: In case full transaction reprocessing, we need to recalculate the schedule as well

This commit is contained in:
Adam Saghy
2025-11-13 08:44:22 +01:00
parent 051571003e
commit c79c076ee6
39 changed files with 361 additions and 479 deletions
@@ -46,7 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.springframework.transaction.annotation.Transactional;
@AllArgsConstructor
@@ -56,8 +56,8 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
private final LoanTermVariationsRepository loanTermVariationsRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanAssembler loanAssembler;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanScheduleService loanScheduleService;
@Override
public CommandProcessingResult createInterestPause(final ExternalId loanExternalId, final String startDateString,
@@ -114,7 +114,7 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
loanTermVariationsRepository.delete(variation);
loan.getLoanTermVariations().remove(variation);
reprocessLoanTransactionsService.reprocessTransactions(loan);
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -141,7 +141,7 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
LoanTermVariations updatedVariation = loanTermVariationsRepository.save(variation);
reprocessLoanTransactionsService.reprocessTransactions(loan);
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsAddedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -161,7 +161,7 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
final LoanTermVariations savedVariation = loanTermVariationsRepository.saveAndFlush(variation);
loan.getLoanTermVariations().add(savedVariation);
reprocessLoanTransactionsService.reprocessTransactions(loan);
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsAddedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -0,0 +1,47 @@
/**
* 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 java.time.LocalDate;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
public interface ILoanUtilService {
ScheduleGeneratorDTO buildScheduleGeneratorDTO(Loan loan, LocalDate recalculateFrom);
ScheduleGeneratorDTO buildScheduleGeneratorDTO(Loan loan, LocalDate recalculateFrom, LocalDate rescheduleTill);
ScheduleGeneratorDTO buildScheduleGeneratorDTO(Loan loan, LocalDate recalculateFrom, LocalDate recalculateTill,
HolidayDetailDTO holidayDetailDTO);
Boolean isLoanRepaymentsSyncWithMeeting(Group group, Calendar calendar);
LocalDate getCalculatedRepaymentsStartingFromDate(Loan loan);
HolidayDetailDTO constructHolidayDTO(Long officeId, LocalDate localDate);
void validateRepaymentTransactionType(LoanTransactionType repaymentTransactionType);
void checkClientOrGroupActive(Loan loan);
}
@@ -94,6 +94,7 @@ public class LoanBalanceService {
public void updateLoanSummaryDerivedFields(final Loan loan) {
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
loan.updateLoanScheduleDependentDerivedFields();
if (loan.isNotDisbursed()) {
if (loan.getSummary() != null) {
loan.getSummary().zeroFields();
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigu
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
@@ -901,4 +902,51 @@ public class LoanChargeService {
return amount;
}
public void removeLoanCharge(final Loan loan, final LoanCharge loanCharge) {
final boolean removed = loanCharge.isActive();
if (removed) {
loanCharge.setActive(false);
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
}
removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loan, loanCharge);
loan.getLoanCharges().remove(loanCharge);
}
private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final Loan loan, final LoanCharge loanCharge) {
if (loanCharge.isDueAtDisbursement()) {
LoanTransaction transactionToRemove = null;
List<LoanTransaction> transactions = loan.getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
if (transaction.isRepaymentAtDisbursement()
&& doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) {
final MonetaryCurrency currency = loan.getCurrency();
final Money chargeAmount = Money.of(currency, loanCharge.amount());
if (transaction.isGreaterThan(chargeAmount)) {
final Money principalPortion = Money.zero(currency);
final Money interestPortion = Money.zero(currency);
final Money penaltyChargesPortion = Money.zero(currency);
transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltyChargesPortion);
} else {
transactionToRemove = transaction;
}
}
}
if (transactionToRemove != null) {
loan.removeLoanTransaction(transactionToRemove);
}
}
}
private boolean doesLoanChargePaidByContainLoanCharge(Set<LoanChargePaidBy> loanChargePaidBys, LoanCharge loanCharge) {
return loanChargePaidBys.stream() //
.anyMatch(loanChargePaidBy -> loanChargePaidBy.getLoanCharge().equals(loanCharge));
}
}
@@ -156,9 +156,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
if (!processLatest || reprocessOnPostConditions) {
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
} else if (loan.isProgressiveSchedule()) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
@@ -42,6 +42,7 @@ public class LoanScheduleService {
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanScheduleComponent loanSchedule;
private final LoanTransactionRepository loanTransactionRepository;
private final ILoanUtilService loanUtilService;
/**
* Ability to regenerate the repayment schedule based on the loans current details/state.
@@ -60,7 +61,7 @@ public class LoanScheduleService {
}
}
public void recalculateSchedule(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
public void regenerateScheduleWithReprocessingTransactions(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargedOff()) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
} else {
@@ -123,11 +124,20 @@ public class LoanScheduleService {
}
}
}
loanTransactionProcessingService.processPostDisbursementTransactions(loan);
}
public void handleRegenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
public void regenerateScheduleWithReprocessingTransactions(Loan loan) {
ScheduleGeneratorDTO generatorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
regenerateScheduleWithReprocessingTransactions(loan, generatorDTO);
}
public void regenerateRepaymentSchedule(final Loan loan) {
ScheduleGeneratorDTO generatorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
regenerateRepaymentSchedule(loan, generatorDTO);
}
}
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
@@ -48,8 +47,6 @@ public interface LoanTransactionProcessingService {
LoanRepaymentScheduleTransactionProcessor getTransactionProcessor(String transactionProcessingStrategyCode);
Optional<ChangedTransactionDetail> processPostDisbursementTransactions(Loan loan);
LoanScheduleDTO getRecalculatedSchedule(ScheduleGeneratorDTO generatorDTO, Loan loan);
OutstandingAmountsDTO fetchPrepaymentDetail(ScheduleGeneratorDTO scheduleGeneratorDTO, LocalDate onDate, Loan loan);
@@ -18,25 +18,17 @@
*/
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 reprocessParticularTransactions(Loan loan, List<LoanTransaction> loanTransactions);
void reprocessTransactions(Loan loan, List<LoanTransaction> loanTransactions);
void reprocessTransactionsWithPostTransactionChecks(Loan loan, LocalDate transactionDate);
void reprocessTransactionsWithoutChecks(Loan loan, LocalDate transactionDate, List<LoanTransaction> newTransactions);
void processPostDisbursementTransactions(Loan loan);
void removeLoanCharge(Loan loan, LoanCharge loanCharge);
void reprocessTransactionsWithoutChecks(Loan loan, List<LoanTransaction> newTransactions);
void processLatestTransaction(LoanTransaction loanTransaction, Loan loan);