diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java index a37485088..965c6c0c7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java @@ -38,7 +38,7 @@ public class LoanScheduleService { private final LoanChargeService loanChargeService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanMapper loanMapper; - private final LoanTransactionProcessingService loadTransactionProcessingService; + private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanScheduleComponent loanSchedule; /** @@ -77,14 +77,16 @@ public class LoanScheduleService { } else { regenerateRepaymentSchedule(loan, generatorDTO); } + reprocessLoanTransactionsService.reprocessTransactions(loan); + } else { + reprocessLoanTransactionsService.updateModel(loan); } - reprocessLoanTransactionsService.reprocessTransactions(loan); } public void regenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) { final LocalDate lastTransactionDate = loan.getLastUserTransactionDate(); - final LoanScheduleDTO loanScheduleDTO = loadTransactionProcessingService.getRecalculatedSchedule(generatorDTO, loan); + final LoanScheduleDTO loanScheduleDTO = loanTransactionProcessingService.getRecalculatedSchedule(generatorDTO, loan); if (loanScheduleDTO == null) { return; } @@ -112,7 +114,7 @@ public class LoanScheduleService { } } } - loadTransactionProcessingService.processPostDisbursementTransactions(loan); + loanTransactionProcessingService.processPostDisbursementTransactions(loan); } public void handleRegenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingService.java index 4f0a35626..4a191bad9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingService.java @@ -18,136 +18,37 @@ */ package org.apache.fineract.portfolio.loanaccount.service; -import java.math.MathContext; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; -import lombok.RequiredArgsConstructor; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; 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.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; 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.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; -import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper; -import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; -import org.springframework.stereotype.Service; -@Service -@RequiredArgsConstructor -public class LoanTransactionProcessingService { +public interface LoanTransactionProcessingService { - private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; - private final LoanTermVariationsMapper loanMapper; + ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction, + TransactionCtx ctx); - public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction, - TransactionCtx ctx) { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( - transactionProcessingStrategyCode); - return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx); - } - - public ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate, + ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate, List repaymentsOrWaivers, MonetaryCurrency currency, - List repaymentScheduleInstallments, Set charges) { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( - transactionProcessingStrategyCode); - return loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(disbursementDate, repaymentsOrWaivers, currency, - repaymentScheduleInstallments, charges); - } + List repaymentScheduleInstallments, Set charges); - public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor(String transactionProcessingStrategyCode) { - return transactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); - } + LoanRepaymentScheduleTransactionProcessor getTransactionProcessor(String transactionProcessingStrategyCode); - public Optional processPostDisbursementTransactions(Loan loan) { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( - loan.getTransactionProcessingStrategyCode()); - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); - final List copyTransactions = new ArrayList<>(); + Optional processPostDisbursementTransactions(Loan loan); - if (allNonContraTransactionsPostDisbursement.isEmpty()) { - return Optional.empty(); - } + LoanScheduleDTO getRecalculatedSchedule(ScheduleGeneratorDTO generatorDTO, Loan loan); - // TODO: Probably this is not needed and can be eliminated, make sure to double check it - for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) { - copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction)); - } - final ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), - loan.getActiveCharges()); + OutstandingAmountsDTO fetchPrepaymentDetail(ScheduleGeneratorDTO scheduleGeneratorDTO, LocalDate onDate, Loan loan); - loan.updateLoanSummaryDerivedFields(); - - return Optional.of(changedTransactionDetail); - } - - public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO, Loan loan) { - if (!loan.isInterestBearingAndInterestRecalculationEnabled() || loan.isNpa() || loan.isChargedOff()) { - return null; - } - final InterestMethod interestMethod = loan.getLoanRepaymentScheduleDetail().getInterestMethod(); - final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory() - .create(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType(), interestMethod); - - final MathContext mc = MoneyHelper.getMathContext(); - - final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(generatorDTO, loan); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( - loan.getTransactionProcessingStrategyCode()); - - return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loan, generatorDTO.getHolidayDetailDTO(), - loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom()); - } - - public OutstandingAmountsDTO fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate, Loan loan) { - OutstandingAmountsDTO outstandingAmounts; - - if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargeOffOnDate(onDate)) { - final MathContext mc = MoneyHelper.getMathContext(); - - final InterestMethod interestMethod = loan.getLoanRepaymentScheduleDetail().getInterestMethod(); - final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan); - - final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory() - .create(loanApplicationTerms.getLoanScheduleType(), interestMethod); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( - loan.getTransactionProcessingStrategyCode()); - outstandingAmounts = loanScheduleGenerator.calculatePrepaymentAmount(loan.getCurrency(), onDate, loanApplicationTerms, mc, loan, - scheduleGeneratorDTO.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor); - } else { - outstandingAmounts = getTotalOutstandingOnLoan(loan); - } - return outstandingAmounts; - } - - private OutstandingAmountsDTO getTotalOutstandingOnLoan(Loan loan) { - Money totalPrincipal = Money.zero(loan.getCurrency()); - Money totalInterest = Money.zero(loan.getCurrency()); - Money feeCharges = Money.zero(loan.getCurrency()); - Money penaltyCharges = Money.zero(loan.getCurrency()); - List repaymentSchedule = loan.getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) { - totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loan.getCurrency())); - totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loan.getCurrency())); - feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loan.getCurrency())); - penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loan.getCurrency())); - } - return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest) - .feeCharges(feeCharges).penaltyCharges(penaltyCharges); - } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java index 936afb709..72bc66f15 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsService.java @@ -38,4 +38,5 @@ public interface ReprocessLoanTransactionsService { void processLatestTransaction(LoanTransaction loanTransaction, Loan loan); + void updateModel(Loan loan); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index c0fab8393..2009e7f40 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -1187,7 +1187,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList(); } - private void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { + public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) { Loan loan = ctx.getInstallments().get(0).getLoan(); if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()) { @@ -1240,17 +1240,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep LocalDate toDate = currentInstallment.getDueDate(); boolean hasUpdate = false; - if (!currentDate.equals(ctx.getLastOverdueBalanceChange())) { + if (!currentDate.equals(ctx.getModel().lastOverdueBalanceChange())) { // if we have same date for fromDate & last overdue balance change then it means we have the up-to-date // model. - if (ctx.getLastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(ctx.getLastOverdueBalanceChange())) { + if (ctx.getModel().lastOverdueBalanceChange() == null + || currentInstallment.getFromDate().isAfter(ctx.getModel().lastOverdueBalanceChange())) { // first overdue hit for installment. setting overdue balance correction from instalment from date. emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal); } else { // not the first balance correction on installment period, then setting overdue balance correction from // last balance change's current date. previous interest period already has the correct balance // correction. - emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getLastOverdueBalanceChange(), overduePrincipal); + emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getModel().lastOverdueBalanceChange(), overduePrincipal); } hasUpdate = true; @@ -1263,7 +1264,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep lastOverdueBalanceChange = currentDate; } emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated()); - ctx.setLastOverdueBalanceChange(lastOverdueBalanceChange); + ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java index 74b12670e..a86b67939 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java @@ -18,7 +18,6 @@ */ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; -import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -38,8 +37,6 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanIntere public class ProgressiveTransactionCtx extends TransactionCtx { private final ProgressiveLoanInterestScheduleModel model; - @Setter - private LocalDate lastOverdueBalanceChange = null; private List alreadyProcessedTransactions = new ArrayList<>(); @Setter private Money sumOfInterestRefundAmount; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java index 76dfd8a0c..a39e202ce 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java @@ -18,16 +18,23 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import java.time.LocalDate; import java.util.Optional; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; public interface InterestScheduleModelRepositoryWrapper { + Optional findOneByLoanId(Long loanId); + + Optional extractModel(Optional progressiveLoanModel); + String writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model); Optional readProgressiveLoanInterestScheduleModel(Long loanId, LoanProductMinimumRepaymentScheduleRelatedDetail detail, Integer installmentAmountInMultipliesOf); + Optional getSavedModel(Loan loan, LocalDate businessDate); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java index 7be230c5b..13cc71fa4 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java @@ -18,14 +18,23 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import jakarta.persistence.EntityManager; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; +import java.time.LocalDate; import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx; import org.apache.fineract.portfolio.loanaccount.repository.ProgressiveLoanModelRepository; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; @@ -37,6 +46,9 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched private final ProgressiveLoanModelRepository loanModelRepository; private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; + private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; + @PersistenceContext + private final EntityManager entityManager; @Transactional @Override @@ -45,18 +57,54 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched return null; } String jsonModel = progressiveLoanInterestScheduleModelParserService.toJson(model); - ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> { - ProgressiveLoanModel plm = new ProgressiveLoanModel(); - plm.setLoan(loan); - return plm; - }); - progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); - progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); - progressiveLoanModel.setJsonModel(jsonModel); - loanModelRepository.save(progressiveLoanModel); + try { + entityManager.setFlushMode(FlushModeType.COMMIT); + ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> { + ProgressiveLoanModel plm = new ProgressiveLoanModel(); + plm.setLoan(loan); + return plm; + }); + progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); + progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); + progressiveLoanModel.setJsonModel(jsonModel); + loanModelRepository.save(progressiveLoanModel); + } finally { + entityManager.setFlushMode(FlushModeType.AUTO); + } return jsonModel; } + @Override + public Optional findOneByLoanId(Long loanId) { + return loanModelRepository.findOneByLoanId(loanId); + } + + @Override + public Optional extractModel(Optional progressiveLoanModel) { + return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) // + .map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, + progressiveLoanModel.get().getLoan().getLoanProductRelatedDetail(), MoneyHelper.getMathContext(), + progressiveLoanModel.get().getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf())); + } + + @Override + public Optional getSavedModel(Loan loan, LocalDate businessDate) { + Optional progressiveLoanModel = findOneByLoanId(loan.getId()); + Optional savedModel; + if (progressiveLoanModel.isPresent() && !progressiveLoanModel.get().getBusinessDate().isAfter(businessDate)) { + savedModel = extractModel(progressiveLoanModel); + if (progressiveLoanModel.get().getBusinessDate().isBefore(businessDate) && savedModel.isPresent()) { + ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get()); + ctx.setChargedOff(loan.isChargedOff()); + advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx); + } + } else { + savedModel = Optional.empty(); + } + return savedModel; + } + @Override public Optional readProgressiveLoanInterestScheduleModel(final Long loanId, final LoanProductMinimumRepaymentScheduleRelatedDetail detail, final Integer installmentAmountInMultipliesOf) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index 51f568b27..e8f655be0 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -41,6 +41,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Setter; import lombok.experimental.Accessors; import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -67,6 +68,9 @@ public class ProgressiveLoanInterestScheduleModel { private final Money zero; private final Map modifiers; + @Setter + private LocalDate lastOverdueBalanceChange; + public ProgressiveLoanInterestScheduleModel(final List repaymentPeriods, final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, final List loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java new file mode 100644 index 000000000..909f7b28f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java @@ -0,0 +1,189 @@ +/** + * 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.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; +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.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; +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.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +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.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ObjectUtils; + +@Service +@RequiredArgsConstructor +public class LoanTransactionProcessingServiceImpl implements LoanTransactionProcessingService { + + private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; + private final LoanTermVariationsMapper loanMapper; + private final InterestScheduleModelRepositoryWrapper modelRepository; + + @Override + public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction, + TransactionCtx ctx) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( + transactionProcessingStrategyCode); + return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx); + } + + private Loan getLoan(List loanTransactions, List installments, + Set charges) { + if (!ObjectUtils.isEmpty(loanTransactions)) { + return loanTransactions.getFirst().getLoan(); + } else if (!ObjectUtils.isEmpty(installments)) { + return installments.getFirst().getLoan(); + } else if (!ObjectUtils.isEmpty(charges)) { + return charges.iterator().next().getLoan(); + } else { + throw new IllegalArgumentException("No loan found for the given transactions, installments or charges"); + } + } + + @Override + public ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate, + List loanTransactions, MonetaryCurrency currency, List installments, + Set charges) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( + transactionProcessingStrategyCode); + if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor) { + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + Pair result = advancedProcessor + .reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges); + if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + modelRepository.writeInterestScheduleModel(getLoan(loanTransactions, installments, charges), result.getRight()); + } + return result.getLeft(); + } else { + return loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(disbursementDate, loanTransactions, currency, + installments, charges); + } + } + + @Override + public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor(String transactionProcessingStrategyCode) { + return transactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); + } + + @Override + public Optional processPostDisbursementTransactions(Loan loan) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( + loan.getTransactionProcessingStrategyCode()); + final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + final List copyTransactions = new ArrayList<>(); + + if (allNonContraTransactionsPostDisbursement.isEmpty()) { + return Optional.empty(); + } + + // TODO: Probably this is not needed and can be eliminated, make sure to double check it + for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) { + copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction)); + } + final ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges()); + + loan.updateLoanSummaryDerivedFields(); + + return Optional.of(changedTransactionDetail); + } + + @Override + public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO, Loan loan) { + if (!loan.isInterestBearingAndInterestRecalculationEnabled() || loan.isNpa() || loan.isChargedOff()) { + return null; + } + final InterestMethod interestMethod = loan.getLoanRepaymentScheduleDetail().getInterestMethod(); + final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory() + .create(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType(), interestMethod); + + final MathContext mc = MoneyHelper.getMathContext(); + + final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(generatorDTO, loan); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( + loan.getTransactionProcessingStrategyCode()); + + return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loan, generatorDTO.getHolidayDetailDTO(), + loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom()); + } + + @Override + public OutstandingAmountsDTO fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate, Loan loan) { + OutstandingAmountsDTO outstandingAmounts; + + if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargeOffOnDate(onDate)) { + final MathContext mc = MoneyHelper.getMathContext(); + + final InterestMethod interestMethod = loan.getLoanRepaymentScheduleDetail().getInterestMethod(); + final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan); + + final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory() + .create(loanApplicationTerms.getLoanScheduleType(), interestMethod); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( + loan.getTransactionProcessingStrategyCode()); + outstandingAmounts = loanScheduleGenerator.calculatePrepaymentAmount(loan.getCurrency(), onDate, loanApplicationTerms, mc, loan, + scheduleGeneratorDTO.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor); + } else { + outstandingAmounts = getTotalOutstandingOnLoan(loan); + } + return outstandingAmounts; + } + + private OutstandingAmountsDTO getTotalOutstandingOnLoan(Loan loan) { + Money totalPrincipal = Money.zero(loan.getCurrency()); + Money totalInterest = Money.zero(loan.getCurrency()); + Money feeCharges = Money.zero(loan.getCurrency()); + Money penaltyCharges = Money.zero(loan.getCurrency()); + List repaymentSchedule = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) { + totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loan.getCurrency())); + totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loan.getCurrency())); + feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loan.getCurrency())); + penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loan.getCurrency())); + } + return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest) + .feeCharges(feeCharges).penaltyCharges(penaltyCharges); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index eb6893b5d..2e9d4823c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -53,6 +53,7 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final EMICalculator emiCalculator; private final LoanRepositoryWrapper loanRepository; + private final InterestScheduleModelRepositoryWrapper modelRepository; @Override public boolean accept(String loanProcessingStrategyCode) { @@ -78,6 +79,24 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro && businessDate.isAfter(i.getFromDate()) && !businessDate.isAfter(i.getDueDate())).findFirst(); } + private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) { + List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() + .filter(t -> !t.isAccrualActivity()).toList(); + Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight(); + final List replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft() + .getTransactionChanges().stream().filter(change -> change.getOldTransaction() != null && change.getNewTransaction() != null) + .map(change -> change.getNewTransaction().getId()).filter(Objects::nonNull).toList(); + + if (!replayedTransactions.isEmpty()) { + log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}", + replayedTransactions); + } + return model; + } + @Override public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, final Collection periods, final LocalDate businessDate, final CurrencyData currency, @@ -93,21 +112,10 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro return MathUtil.subtractToZero(currentRepaymentPeriod.get().getInterestOutstanding(loan.getCurrency()).getAmount(), totalUnpaidPayableDueInterest); } else { - List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() - .filter(t -> !t.isAccrualActivity()).toList(); - Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor - .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, - loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight(); - final List replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft() - .getTransactionChanges().stream() - .filter(change -> change.getOldTransaction() != null && change.getNewTransaction() != null) - .map(change -> change.getNewTransaction().getId()).filter(Objects::nonNull).toList(); - if (!replayedTransactions.isEmpty()) { - log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}", - replayedTransactions); - } + Optional savedModel = modelRepository.getSavedModel(loan, businessDate); + + ProgressiveLoanInterestScheduleModel model = savedModel.orElseGet(() -> calculateModel(loan, businessDate)); if (model != null) { OutstandingDetails outstandingDetails = emiCalculator.getOutstandingAmountsTillDate(model, businessDate); if (!loan.isInterestRecalculationEnabled()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java similarity index 89% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index e39b0f57f..1d4612ce9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -20,7 +20,9 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.interestpauses.service.LoanAccountTransfersService; import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; @@ -31,6 +33,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentSchedulePro import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.springframework.stereotype.Service; @Service @@ -40,7 +43,8 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa private final LoanAccountService loanAccountService; private final LoanAccountTransfersService loanAccountTransfersService; private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; - private final LoanTransactionProcessingService loadTransactionProcessingService; + private final LoanTransactionProcessingService loanTransactionProcessingService; + private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper; @Override public void reprocessTransactions(final Loan loan) { @@ -65,7 +69,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa @Override public void processPostDisbursementTransactions(final Loan loan) { - loadTransactionProcessingService.processPostDisbursementTransactions(loan).ifPresent(this::handleChangedDetail); + loanTransactionProcessingService.processPostDisbursementTransactions(loan).ifPresent(this::handleChangedDetail); } @Override @@ -98,7 +102,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa @Override public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) { - final ChangedTransactionDetail changedTransactionDetail = loadTransactionProcessingService.processLatestTransaction( + final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction( loan.getTransactionProcessingStrategyCode(), loanTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail())); @@ -110,6 +114,15 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa handleChangedDetail(changedTransactionDetail); } + @Override + public void updateModel(Loan loan) { + Optional savedModel = interestScheduleModelRepositoryWrapper.getSavedModel(loan, + ThreadLocalContextUtil.getBusinessDate()); + if (savedModel.isEmpty()) { + reprocessTransactions(loan); + } + } + private void handleChangedDetail(final ChangedTransactionDetail changedTransactionDetail) { for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { final LoanTransaction newTransaction = change.getNewTransaction(); @@ -126,7 +139,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa private ChangedTransactionDetail reprocessTransactionsAndFetchChangedTransactions(final Loan loan, final List loanTransactions) { - final ChangedTransactionDetail changedTransactionDetail = loadTransactionProcessingService.reprocessLoanTransactions( + final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.reprocessLoanTransactions( loan.getTransactionProcessingStrategyCode(), loan.getDisbursementDate(), loanTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); for (TransactionChangeData change : changedTransactionDetail.getTransactionChanges()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java index 71b21378c..19f0e11d0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java @@ -23,17 +23,33 @@ import java.util.List; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class LoanSummaryTest extends BaseLoanIntegrationTest { + private static BusinessStepHelper.BusinessStepsSnapshot originalConfig; + private static final BusinessStepHelper businessStepHelper = new BusinessStepHelper(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long loanId; + @BeforeAll + static void setup() { + originalConfig = businessStepHelper.getConfigurationSnapshot("LOAN_CLOSE_OF_BUSINESS"); + } + + @AfterAll + public static void teardown() { + originalConfig.restore(); + } + @Test public void testUnpaidPayableNotDueInterestForProgressiveLoanInCaseOfEarlyRepayment() { + businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "ADD_PERIODIC_ACCRUAL_ENTRIES", "LOAN_INTEREST_RECALCULATION"); runAt("1 January 2024", () -> { final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, @@ -98,6 +114,7 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { @Test public void testUnpaidPayableNotDueInterestForProgressiveLoanInCaseOfEarlyRepaymentAlmostFullyPaid2ndPeriod() { + businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "LOAN_INTEREST_RECALCULATION"); runAt("15 March 2025", () -> { final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( create4IProgressive().interestRatePerPeriod(35.99).numberOfRepayments(12).isInterestRecalculationEnabled(true)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java index 3e6aa001c..ed0c17a92 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java @@ -18,10 +18,16 @@ */ package org.apache.fineract.integrationtests; +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +@Slf4j public class ProgressiveLoanModelIntegrationTest extends BaseLoanIntegrationTest { private final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); @@ -29,26 +35,56 @@ public class ProgressiveLoanModelIntegrationTest extends BaseLoanIntegrationTest .getResourceId(); @Test - public void testModelReturnsNullAndThenSaveThenNotNull() { + public void testModelReturnsSavedModelAfterDisbursement() { + AtomicLong loanIdA = new AtomicLong(); runAt("1 January 2024", () -> { Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "1 January 2024", 1000.0, 96.32, 6, null); + loanIdA.set(loanId); + log.info("Testing on loanId: {}", loanId); loanTransactionHelper.disburseLoan(loanId, "1 January 2024", 1000.0); + // Model saved automatically, fetching it. It should return non null + ProgressiveLoanInterestScheduleModel model = assertNotNullAndChanged(null, loanId); + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "1 January 2024", 12.78); + model = assertNotNullAndChanged(model, loanId); + assertNotNullAndSame(model, loanId); + }); + runAt("28 February 2024", () -> { + Long loanId = loanIdA.get(); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + BigDecimal totalUnpaidPayableNotDueInterest = loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest(); + BigDecimal totalUnpaidDueInterest = loanDetails.getSummary().getTotalUnpaidPayableDueInterest(); - // Model not saved, fetching it. It should return null - ProgressiveLoanInterestScheduleModel progressiveLoanInterestScheduleModelResponse1 = ok( - fineractClient().progressiveLoanApi.fetchModel(loanId)); - - assertThat(progressiveLoanInterestScheduleModelResponse1).isNull(); - - // Forcing Model recalculation and save to database. It should return the actual model. - ProgressiveLoanInterestScheduleModel ok = ok(fineractClient().progressiveLoanApi.updateModel(loanId)); - assertThat(ok).isNotNull(); - - // Model saved in previous step. API should return the previous model. - ProgressiveLoanInterestScheduleModel progressiveLoanInterestScheduleModelResponse2 = ok( - fineractClient().progressiveLoanApi.fetchModel(loanId)); - assertThat(progressiveLoanInterestScheduleModelResponse2).isNotNull(); + Assertions.assertEquals(73.78, totalUnpaidPayableNotDueInterest.doubleValue(), 0.001); + Assertions.assertEquals(79.24, totalUnpaidDueInterest.doubleValue(), 0.001); }); } + private ProgressiveLoanInterestScheduleModel assertNotNullAndChanged(ProgressiveLoanInterestScheduleModel prev, Long loanId) { + return assertNotNullAndChanged(prev, ok(fineractClient().progressiveLoanApi.fetchModel(loanId))); + } + + private ProgressiveLoanInterestScheduleModel assertNotNullAndSame(ProgressiveLoanInterestScheduleModel prev, Long loanId) { + return assertNotNullAndSame(prev, ok(fineractClient().progressiveLoanApi.fetchModel(loanId))); + } + + private ProgressiveLoanInterestScheduleModel assertNotNullAndSame(ProgressiveLoanInterestScheduleModel prev, + ProgressiveLoanInterestScheduleModel actual) { + if (actual == null) { + Assertions.fail("Model is null"); + } + Assertions.assertEquals(prev.toString(), actual.toString()); + return actual; + } + + private ProgressiveLoanInterestScheduleModel assertNotNullAndChanged(ProgressiveLoanInterestScheduleModel prev, + ProgressiveLoanInterestScheduleModel actual) { + if (actual == null) { + Assertions.fail("Model is null"); + } + if (prev != null) { + Assertions.assertNotEquals(prev.toString(), actual.toString()); + } + return actual; + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java index 0be73f0df..1b23fd6f2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java @@ -20,6 +20,8 @@ package org.apache.fineract.integrationtests.common; import java.util.ArrayList; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; import org.apache.fineract.client.models.BusinessStep; import org.apache.fineract.client.models.BusinessStepRequest; import org.apache.fineract.client.util.Calls; @@ -28,6 +30,13 @@ public class BusinessStepHelper { public BusinessStepHelper() {} + public BusinessStepsSnapshot getConfigurationSnapshot(String jobName) { + List businessSteps = Calls + .ok(FineractClientHelper.getFineractClient().businessStepConfiguration.retrieveAllConfiguredBusinessStep(jobName)) + .getBusinessSteps(); + return new BusinessStepsSnapshot(jobName, businessSteps); + } + public void updateSteps(String jobName, String... steps) { long order = 0; List stepList = new ArrayList<>(); @@ -41,4 +50,17 @@ public class BusinessStepHelper { Calls.ok(FineractClientHelper.getFineractClient().businessStepConfiguration.updateJobBusinessStepConfig(jobName, new BusinessStepRequest().businessSteps(stepList))); } + + @Getter + @AllArgsConstructor + public static class BusinessStepsSnapshot { + + private String jobName; + private List businessSteps; + + public void restore() { + Calls.ok(FineractClientHelper.getFineractClient().businessStepConfiguration.updateJobBusinessStepConfig(jobName, + new BusinessStepRequest().businessSteps(businessSteps))); + } + } }