From 30f53f7489d035597e1c08eab9bd4af3735f7f95 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Thu, 5 Dec 2024 18:14:42 +0200 Subject: [PATCH] FINERACT-2148: update instalments interest with zero after charge off with interest recalculation enabled --- ...WritePlatformServiceJpaRepositoryImpl.java | 106 +++++++++++++++++- .../starter/LoanAccountConfiguration.java | 11 +- 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index a747373c73..f7f77c6cf5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -32,8 +32,10 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.github.resilience4j.retry.annotation.Retry; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -109,6 +111,9 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; @@ -154,6 +159,7 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; @@ -161,6 +167,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; @@ -184,6 +191,7 @@ 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.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; @@ -198,6 +206,9 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -209,6 +220,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; +import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; @@ -279,12 +291,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final AccountTransferRepository accountTransferRepository; private final LoanTransactionAssembler loanTransactionAssembler; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final LoanOfficerValidator loanOfficerValidator; private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; private final LoanDisbursementService loanDisbursementService; private final LoanScheduleService loanScheduleService; private final LoanChargeValidator loanChargeValidator; private final LoanOfficerService loanOfficerService; + private final ProgressiveEMICalculator emiCalculator; + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; @Transactional @Override @@ -3260,7 +3275,43 @@ public CommandProcessingResult chargeOff(JsonCommand command) { final List existingTransactionIds = loan.findExistingTransactionIds(); final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); - LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId); + if (LoanChargeOffBehaviour.ZERO_INTEREST.equals(loan.getLoanProductRelatedDetail().getChargeOffBehaviour())) { + final List repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); + + if (loan.isInterestRecalculationEnabled()) { + final Pair result = advancedPaymentScheduleTransactionProcessor + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), transactionDate, + loan.retrieveListOfTransactionsForReprocessing(), loan.getLoanRepaymentScheduleDetail().getCurrency(), + repaymentScheduleInstallments, loan.getActiveCharges()); + + final ProgressiveLoanInterestScheduleModel scheduleModel = result.getRight(); + + repaymentScheduleInstallments.stream() + .filter(installment -> !installment.getFromDate().isAfter(transactionDate) && installment.getDueDate().isAfter(transactionDate)) + .forEach(installment -> { + final BigDecimal newInterest = emiCalculator + .getPeriodInterestTillDate(scheduleModel, installment.getDueDate(), transactionDate).getAmount(); + final BigDecimal interestRemoved = installment.getInterestCharged().subtract(newInterest); + installment.updatePrincipal(defaultToZeroIfNull(installment.getPrincipal()).add(interestRemoved)); + installment.updateInterestCharged(newInterest); + }); + } else { + calculatePartialPeriodInterest(loan, transactionDate); + } + + repaymentScheduleInstallments.stream() + .filter(installment -> installment.getFromDate().isAfter(transactionDate)) + .forEach(installment -> { + installment.updatePrincipal(defaultToZeroIfNull(installment.getPrincipal()).add(installment.getInterestCharged())); + installment.updateInterestCharged(BigDecimal.ZERO); + }); + + loan.updateLoanSummaryDerivedFields(); + + updateLastInstallmentPrincipal(loan, repaymentScheduleInstallments); + } + + final LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId); if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled() && DateUtils.isBefore(loan.getInterestRecalculatedOn(), DateUtils.getBusinessLocalDate())) { @@ -3292,6 +3343,7 @@ public CommandProcessingResult chargeOff(JsonCommand command) { postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(chargeOffTransaction.getId()) // @@ -3902,4 +3954,56 @@ public void closeAsMarkedForReschedule(final Loan loan, final JsonCommand comman loanTransactionValidator.validateLoanRescheduleDate(loan); } + + private void calculatePartialPeriodInterest(final Loan loan, final LocalDate chargeOffDate) { + loan.getRepaymentScheduleInstallments().stream() + .filter(installment -> !installment.getFromDate().isAfter(chargeOffDate) && installment.getDueDate().isAfter(chargeOffDate)) + .forEach(installment -> { + final BigDecimal totalInterest = installment.getInterestOutstanding(loan.getCurrency()).getAmount(); + final long totalDaysInPeriod = ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate()); + final long daysTillChargeOff = ChronoUnit.DAYS.between(installment.getFromDate(), chargeOffDate); + + final BigDecimal interestTillChargeOff = totalInterest + .divide(BigDecimal.valueOf(totalDaysInPeriod), 10, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(daysTillChargeOff)); + + final BigDecimal interestRemoved = totalInterest.subtract(interestTillChargeOff); + installment.updatePrincipal(defaultToZeroIfNull(installment.getPrincipal()).add(interestRemoved)); + installment.updateInterestCharged(interestTillChargeOff); + }); + } + + private BigDecimal defaultToZeroIfNull(final BigDecimal possibleNullValue) { + return possibleNullValue != null ? possibleNullValue : BigDecimal.ZERO; + } + + private void updateLastInstallmentPrincipal(final Loan loan, + final List repaymentScheduleInstallments) { + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(loan.getCurrency()); + final CurrencyData currencyData = new CurrencyData(loan.getCurrencyCode(), applicationCurrency.getName(), + loan.getCurrency().getDigitsAfterDecimal(), applicationCurrency.getCurrencyInMultiplesOf(), + applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode()); + final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData( + loan.getExpectedDisbursementDate(), loan.getActualDisbursementDate(), currencyData, loan.getPrincipal().getAmount(), + loan.getInArrearsTolerance().getAmount(), + loan.getSummary().getTotalFeeChargesDueAtDisbursement(loan.getCurrency()).getAmount()); + final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(loan.getId(), + repaymentScheduleRelatedData, loan.getDisbursementData(), loan.isInterestRecalculationEnabled(), + loan.getLoanRepaymentScheduleDetail().getLoanScheduleType()); + + final int installmentsCount = repaymentScheduleInstallments.size(); + repaymentScheduleInstallments.get(installmentsCount - 1) + .updatePrincipal(repaymentSchedule.getPeriods().stream() + .filter(period -> period.getPeriod() != null && period.getPeriod() == (installmentsCount - 1)).findFirst().get() + .getPrincipalLoanBalanceOutstanding()); + + final int lastInstallmentIndex = repaymentScheduleInstallments.size() - 1; + final Optional preLastPeriod = repaymentSchedule.getPeriods().stream() + .filter(period -> period.getPeriod() != null && period.getPeriod() == lastInstallmentIndex).findFirst(); + + preLastPeriod.ifPresent(loanSchedulePeriodData -> repaymentScheduleInstallments.get(lastInstallmentIndex) + .updatePrincipal(loanSchedulePeriodData.getPrincipalLoanBalanceOutstanding())); + + loan.updateLoanSummaryDerivedFields(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index fdfaa60760..181c5987ab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -75,6 +75,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; @@ -135,6 +136,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl; +import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; @@ -380,7 +382,9 @@ public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTr LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService, LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService, - LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) { + LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService, + AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor, + ProgressiveEMICalculator progressiveEMICalculator, ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository) { return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, @@ -394,8 +398,9 @@ public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTr postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, - loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator, - loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService); + loanTransactionAssembler, loanAccrualsProcessingService, advancedPaymentScheduleTransactionProcessor, loanOfficerValidator, + loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, + progressiveEMICalculator, applicationCurrencyRepository); } @Bean