From 4c0c0d269ef82e273132924ab45f3e4aa49f4fc1 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 12 Feb 2026 22:17:15 +0100 Subject: [PATCH] FINERACT-2421: Allow ProgressiveLoanInterestScheduleModel recalculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Soma Sörös --- .../test/stepdef/loan/LoanStepDef.java | 7 + .../features/LoanAccrualTransaction.feature | 2 + .../loanaccount/domain/LoanRepository.java | 6 + .../domain/LoanRepositoryWrapper.java | 4 + ...dableProgressiveLoanScheduleGenerator.java | 5 + .../domain/ProgressiveLoanModel.java | 3 + .../ProgressiveLoanModelRepository.java | 7 + ...nterestScheduleModelRepositoryWrapper.java | 2 + ...estScheduleModelRepositoryWrapperImpl.java | 7 + .../InternalProgressiveLoanApiResource.java | 10 + ...ProgressiveLoanModelProcessingService.java | 61 +++++ ...gressiveLoanModelRecalculationService.java | 61 +++++ .../ProgressiveLoanInterestScheduleModel.java | 5 + .../module-changelog-master.xml | 1 + .../parts/5003_add_model_version.xml | 35 +++ .../cob/loan/AbstractLoanItemProcessor.java | 15 +- .../cob/loan/InlineCOBLoanItemProcessor.java | 6 +- .../cob/loan/LoanCOBWorkerConfiguration.java | 6 +- .../cob/loan/LoanInlineCOBConfig.java | 6 +- .../fineract/cob/loan/LoanItemProcessor.java | 6 +- .../core/config/SecurityConfig.java | 5 + .../ProgressiveLoanModelCheckerFilter.java | 80 +++++++ .../ProgressiveLoanModelCheckerHelper.java | 217 ++++++++++++++++++ .../config/AuthorizationServerConfig.java | 7 + .../LoanItemProcessorStepDefinitions.java | 4 +- .../integrationtests/common/BatchHelper.java | 10 +- 26 files changed, 563 insertions(+), 15 deletions(-) create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelProcessingService.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelRecalculationService.java create mode 100644 fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5003_add_model_version.xml create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerFilter.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerHelper.java diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 9d7d26e098c..23dcb365d3a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -5926,6 +5926,13 @@ public void inLoanTransactionsTheThTransactionOfOnHasRelationshipWithTypeREPLAYE assertEquals(Integer.valueOf(numberOfRelations), relationshipOptional.size(), "Missed relationship for transaction"); } + @When("Call Internal API to remove progressive loan model by loan Id") + public void callInternalAPIToRemoveProgressiveLoanModelByLoanId() { + final PostLoansResponse loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanCreateResponse.getLoanId(); + ok(() -> fineractClient.progressiveLoan().deleteModel(loanId)); + } + public static AdvancedPaymentData editPaymentAllocationFutureInstallment(String transactionType, String futureInstallmentAllocationRule, List paymentAllocationOrder) { AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index 3c831869ddd..df4c814e6af 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -1619,6 +1619,7 @@ Feature: LoanAccrualTransaction | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | # --- Early repayment with 17.01 EUR on 15 Jan --- When Admin sets the business date to "15 January 2024" + When Call Internal API to remove progressive loan model by loan Id When Admin makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "15 January 2024" with 17.01 EUR transaction amount Then Loan Repayment schedule has 6 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | @@ -1660,6 +1661,7 @@ Feature: LoanAccrualTransaction | 01 May 2024 | Accrual Activity | 0.48 | 0.0 | 0.48 | 0.0 | 0.0 | 0.0 | false | false | | 31 May 2024 | Accrual | 1.87 | 0.0 | 1.87 | 0.0 | 0.0 | 0.0 | false | false | When Admin sets the business date to "02 June 2024" + When Call Internal API to remove progressive loan model by loan Id And Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 0cb93c10388..05c98ee9263 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -279,4 +279,10 @@ List findLoansForAddAccrual(@Param("accountingType") AccountingRuleType ac @Query(FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE) List findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate cobBusinessDate, @Param("loanIds") List loanIds, @Param("loanStatuses") Collection loanStatuses); + + @Query("SELECT CASE WHEN COUNT(l) > 0 THEN TRUE ELSE FALSE END FROM Loan l WHERE l.id = :loanId and l.loanRepaymentScheduleDetail.loanScheduleType = org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.PROGRESSIVE") + Boolean isProgressiveLoan(@Param("loanId") Long loanId); + + @Query("SELECT CASE WHEN COUNT(l) > 0 THEN TRUE ELSE FALSE END FROM Loan l WHERE l.id = :loanId and l.loanStatus in :allowedLoanStatuses") + Boolean isLoanInAllowedStatus(@Param("loanId") Long loanId, @Param("allowedLoanStatuses") List allowedLoanStatuses); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 7943c957153..66241087fb6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -313,4 +313,8 @@ public boolean existsByLoanId(Long loanId) { public boolean isEnabledCapitalizedIncome(Long loanId) { return repository.isEnabledCapitalizedIncome(loanId); } + + public boolean isLoanInAllowedStatus(Long loanId, List allowedLoanStatuses) { + return repository.isLoanInAllowedStatus(loanId, allowedLoanStatuses); + } } diff --git a/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java index 864a20c3e17..79f97b75661 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java @@ -83,5 +83,10 @@ public boolean hasValidModelForDate(Long loanId, LocalDate targetDate) { public Optional getSavedModel(Loan loan, LocalDate businessDate) { return Optional.empty(); } + + @Override + public Long removeByLoanId(Long loanId) { + return 0L; + } } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java index c8aec9b2fda..5ac311c8607 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java @@ -52,4 +52,7 @@ public class ProgressiveLoanModel extends AbstractPersistableCustom { @Column(name = "last_modified_on_utc", nullable = false) private OffsetDateTime lastModifiedDate; + @Column(name = "json_model_version", nullable = false) + private String jsonModelVersion; + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java index be6af1aac27..41066002059 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java @@ -23,6 +23,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ProgressiveLoanModelRepository extends JpaSpecificationExecutor, JpaRepository { @@ -30,4 +32,9 @@ public interface ProgressiveLoanModelRepository Optional findOneByLoanId(Long loanId); Optional findOneByLoan(Loan loan); + + Long removeByLoanId(Long loanId); + + @Query("SELECT CASE WHEN COUNT(plm) > 0 THEN TRUE ELSE FALSE END FROM ProgressiveLoanModel plm WHERE plm.loan.id = :loanId AND plm.jsonModelVersion = :modelVersion") + Boolean hasValidModel(@Param("loanId") Long loanId, @Param("modelVersion") String modelVersion); } 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 1ce74910b7c..977fa53dc12 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 @@ -41,4 +41,6 @@ Optional readProgressiveLoanInterestSchedu boolean hasValidModelForDate(Long loanId, LocalDate targetDate); Optional getSavedModel(Loan loan, LocalDate businessDate); + + Long removeByLoanId(Long loanId); } 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 654af32509f..97683efefcd 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 @@ -61,11 +61,13 @@ public ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> { ProgressiveLoanModel plm = new ProgressiveLoanModel(); plm.setLoan(loan); + plm.setJsonModelVersion(ProgressiveLoanInterestScheduleModel.getModelVersion()); return plm; }); progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); progressiveLoanModel.setJsonModel(jsonModel); + progressiveLoanModel.setJsonModelVersion(ProgressiveLoanInterestScheduleModel.getModelVersion()); loanModelRepository.save(progressiveLoanModel); }); return model; @@ -133,4 +135,9 @@ public Optional readProgressiveLoanInteres .map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, detail, MoneyHelper.getMathContext(), installmentAmountInMultipliesOf)); // } + + @Override + public Long removeByLoanId(Long loanId) { + return loanModelRepository.removeByLoanId(loanId); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java index 4fca8a38f87..ad05ecf43ad 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -99,4 +100,13 @@ public ProgressiveLoanInterestScheduleModel updateModel(@PathParam("loanId") @Pa return writePlatformService.writeInterestScheduleModel(loan, model); } + + @DELETE + @Path("{loanId}/model") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete ProgressiveLoanInterestScheduleModel By Loan ID", description = "DO NOT USE THIS IN PRODUCTION!") + @Transactional + public Long deleteModel(@PathParam("loanId") @Parameter(description = "loanId") long loanId) { + return writePlatformService.removeByLoanId(loanId); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelProcessingService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelProcessingService.java new file mode 100644 index 00000000000..4cedc8a6ccc --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelProcessingService.java @@ -0,0 +1,61 @@ +/** + * 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.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.repository.ProgressiveLoanModelRepository; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class ProgressiveLoanModelProcessingService { + + private static final List allowedLoanStatuses = List.of(LoanStatus.ACTIVE, LoanStatus.CLOSED_OBLIGATIONS_MET, + LoanStatus.CLOSED_WRITTEN_OFF, LoanStatus.OVERPAID); + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final ProgressiveLoanModelRecalculationService modelProcessingService; + private final InterestScheduleModelRepositoryWrapper modelRepositoryWrapper; + private final ProgressiveLoanModelRepository progressiveLoanModelRepository; + + public boolean hasValidModel(Long loanId, String modelVersion) { + return progressiveLoanModelRepository.hasValidModel(loanId, modelVersion); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void recalculateModelAndSave(Long loanId) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + ProgressiveLoanInterestScheduleModel recalculatedModel = modelProcessingService.getRecalculatedModel(loan.getId(), + ThreadLocalContextUtil.getBusinessDate()); + if (recalculatedModel != null) { + modelRepositoryWrapper.writeInterestScheduleModel(loan, recalculatedModel); + } + } + + public boolean allowedLoanStatuses(Long loanId) { + return loanRepositoryWrapper.isLoanInAllowedStatus(loanId, allowedLoanStatuses); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelRecalculationService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelRecalculationService.java new file mode 100644 index 00000000000..66e41f573c6 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanModelRecalculationService.java @@ -0,0 +1,61 @@ +/** + * 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 java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class ProgressiveLoanModelRecalculationService { + + private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; + private final LoanTransactionRepository loanTransactionRepository; + private final LoanRepositoryWrapper loanRepositoryWrapper; + + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + public ProgressiveLoanInterestScheduleModel getRecalculatedModel(Long loanId, LocalDate tillDate) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + LoanRepaymentScheduleTransactionProcessor transactionProcessor = transactionProcessorFactory + .determineProcessor(loan.getTransactionProcessingStrategyCode()); + if (transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor) { + List loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); + Pair result = advancedPaymentScheduleTransactionProcessor + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), tillDate, loanTransactions, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + return result.getRight(); + } + return null; + } + +} 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 55d9f000bfc..c479d6d3a1b 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 @@ -54,6 +54,7 @@ @AllArgsConstructor public class ProgressiveLoanInterestScheduleModel { + private static final String modelVersion = "2"; private final List repaymentPeriods; private final TreeSet interestRates; @JsonExclude @@ -419,4 +420,8 @@ public Function resolveRepaymentPeriodLengthGeneratorFunction(f }; } + public static String getModelVersion() { + return modelVersion; + } + } diff --git a/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml index c0f34fbf242..a18949ec160 100644 --- a/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml +++ b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml @@ -25,4 +25,5 @@ + diff --git a/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5003_add_model_version.xml b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5003_add_model_version.xml new file mode 100644 index 00000000000..cb601b8c61c --- /dev/null +++ b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5003_add_model_version.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java index b5c7f544c2d..42f140b3a6e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java @@ -32,6 +32,8 @@ import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.cob.data.BusinessStepNameAndOrder; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.AfterStep; @@ -44,6 +46,7 @@ public abstract class AbstractLoanItemProcessor implements ItemProcessor { private final COBBusinessStepService cobBusinessStepService; + private final ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; @Setter(AccessLevel.PROTECTED) private ExecutionContext executionContext; @@ -51,18 +54,26 @@ public abstract class AbstractLoanItemProcessor implements ItemProcessor businessSteps = (Set) executionContext.get(LoanCOBConstant.BUSINESS_STEPS); if (businessSteps == null) { throw new IllegalStateException("No business steps found in the execution context"); } TreeMap businessStepMap = getBusinessStepMap(businessSteps); - Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, item); + Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, loan); alreadyProcessedLoan.setLastClosedBusinessDate(businessDate); return alreadyProcessedLoan; } + private boolean needToRebuildModel(Loan loan) { + return loan.isProgressiveSchedule() && !progressiveLoanModelProcessingService.hasValidModel(loan.getId(), + ProgressiveLoanInterestScheduleModel.getModelVersion()); + } + private TreeMap getBusinessStepMap(Set businessSteps) { Map businessStepMap = businessSteps.stream() .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java index 75894dd4cc9..4326d84d98e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java @@ -19,13 +19,15 @@ package org.apache.fineract.cob.loan; import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.BeforeStep; public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor { - public InlineCOBLoanItemProcessor(COBBusinessStepService cobBusinessStepService) { - super(cobBusinessStepService); + public InlineCOBLoanItemProcessor(COBBusinessStepService cobBusinessStepService, + ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) { + super(cobBusinessStepService, progressiveLoanModelProcessingService); } @BeforeStep diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java index f2e9bc43037..17d97f57090 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java @@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -80,6 +81,9 @@ public class LoanCOBWorkerConfiguration { @Autowired private LoanLockingService loanLockingService; + @Autowired + private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; + @Bean(name = LoanCOBConstant.LOAN_COB_WORKER_STEP) public Step loanCOBWorkerStep(Flow cobFlow) { return stepBuilderFactory.get("Loan COB worker - Step").inputChannel(inboundRequests).flow(cobFlow).build(); @@ -178,7 +182,7 @@ public LoanItemReader cobWorkerItemReader() { @Bean @StepScope public LoanItemProcessor cobWorkerItemProcessor() { - return new LoanItemProcessor(cobBusinessStepService); + return new LoanItemProcessor(cobBusinessStepService, progressiveLoanModelProcessingService); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java index 343c074ed38..bd00a20ad3e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java @@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobScope; @@ -65,9 +66,10 @@ public class LoanInlineCOBConfig { private CustomJobParameterRepository customJobParameterRepository; @Autowired private CustomJobParameterResolver customJobParameterResolver; - @Autowired private LoanLockingService loanLockingService; + @Autowired + private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; @Bean public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() { @@ -106,7 +108,7 @@ public InlineCOBLoanItemReader inlineCobWorkerItemReader() { @JobScope @Bean public InlineCOBLoanItemProcessor inlineCobWorkerItemProcessor() { - return new InlineCOBLoanItemProcessor(cobBusinessStepService); + return new InlineCOBLoanItemProcessor(cobBusinessStepService, progressiveLoanModelProcessingService); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java index 2aa95d27b41..4e7c340d5c0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java @@ -19,13 +19,15 @@ package org.apache.fineract.cob.loan; import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.BeforeStep; public class LoanItemProcessor extends AbstractLoanItemProcessor { - public LoanItemProcessor(COBBusinessStepService cobBusinessStepService) { - super(cobBusinessStepService); + public LoanItemProcessor(COBBusinessStepService cobBusinessStepService, + ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) { + super(cobBusinessStepService, progressiveLoanModelProcessingService); } @BeforeStep diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 438923e6f42..f510cef4253 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; +import org.apache.fineract.infrastructure.jobs.filter.ProgressiveLoanModelCheckerFilter; import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter; import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter; @@ -113,6 +114,8 @@ public class SecurityConfig { private LoanCOBFilterHelper loanCOBFilterHelper; @Autowired private IdempotencyStoreHelper idempotencyStoreHelper; + @Autowired + ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -216,8 +219,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { if (loanCOBFilterHelper != null) { http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class).addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); + http.addFilterBefore(progressiveLoanModelCheckerFilter, LoanCOBApiFilter.class); } else { http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); + http.addFilterAfter(progressiveLoanModelCheckerFilter, FineractInstanceModeApiFilter.class); } if (fineractProperties.getIpTracking().isEnabled()) { http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerFilter.java new file mode 100644 index 00000000000..a47a7ae4c5f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerFilter.java @@ -0,0 +1,80 @@ +/** + * 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.infrastructure.jobs.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class ProgressiveLoanModelCheckerFilter extends OncePerRequestFilter { + + private final LoanRepository loanRepository; + private final ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; + private final ProgressiveLoanModelCheckerHelper helper; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + request = new BodyCachingHttpServletRequestWrapper(request); + + if (!helper.isOnApiList((BodyCachingHttpServletRequestWrapper) request)) { + proceed(filterChain, request, response); + } else { + List loanIds = helper.calculateRelevantLoanIds((BodyCachingHttpServletRequestWrapper) request); + if (!loanIds.isEmpty()) { + loanIds.forEach(loanId -> { + if (isProgressiveLoan(loanId) && allowedLoanStatuses(loanId) && !hasValidModel(loanId)) { + progressiveLoanModelProcessingService.recalculateModelAndSave(loanId); + } + }); + } + proceed(filterChain, request, response); + } + } + + private boolean isProgressiveLoan(Long loanId) { + return loanRepository.isProgressiveLoan(loanId); + } + + private boolean allowedLoanStatuses(Long loanId) { + return progressiveLoanModelProcessingService.allowedLoanStatuses(loanId); + } + + private boolean hasValidModel(Long loanId) { + return progressiveLoanModelProcessingService.hasValidModel(loanId, ProgressiveLoanInterestScheduleModel.getModelVersion()); + } + + private void proceed(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + filterChain.doFilter(request, response); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerHelper.java new file mode 100644 index 00000000000..c6793fb6da6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/ProgressiveLoanModelCheckerHelper.java @@ -0,0 +1,217 @@ +/** + * 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.infrastructure.jobs.filter; + +import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; +import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ProgressiveLoanModelCheckerHelper implements InitializingBean { + + private final GLIMAccountInfoRepository glimAccountInfoRepository; + private final LoanRepository loanRepository; + + private final LoanRescheduleRequestRepository loanRescheduleRequestRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private static final List HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE); + + public static final Pattern IGNORE_LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/catch-up"); + public static final Pattern LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/(?:reschedule)?loans/(?:external-id/)?([^/?]+).*"); + + public static final Pattern LOAN_GLIMACCOUNT_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/glimAccount/(\\d+).*"); + private static final Predicate URL_FUNCTION = s -> LOAN_PATH_PATTERN.matcher(s).find() + || LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find(); + + private Long getLoanId(boolean isGlim, String pathInfo) { + if (!isGlim) { + String id = LOAN_PATH_PATTERN.matcher(pathInfo).replaceAll("$1"); + if (isExternal(pathInfo)) { + String externalId = id; + return loanRepository.findIdByExternalId(new ExternalId(externalId)); + } else if (isRescheduleLoans(pathInfo)) { + return loanRescheduleRequestRepository.getLoanIdByRescheduleRequestId(Long.valueOf(id)).orElse(null); + } else if (StringUtils.isNumeric(id)) { + return Long.valueOf(id); + } else { + return null; + } + } else { + return Long.valueOf(LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).replaceAll("$1")); + } + } + + private boolean isExternal(String pathInfo) { + return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("external-id"); + } + + private boolean isRescheduleLoans(String pathInfo) { + return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/"); + } + + public boolean isOnApiList(BodyCachingHttpServletRequestWrapper request) throws IOException { + String pathInfo = request.getPathInfo(); + String method = request.getMethod(); + if (StringUtils.isBlank(pathInfo)) { + return false; + } + if (isBatchApi(pathInfo)) { + return isBatchApiMatching(request); + } else { + return isApiMatching(method, pathInfo); + } + } + + private boolean isBatchApiMatching(BodyCachingHttpServletRequestWrapper request) throws IOException { + for (BatchRequest batchRequest : getBatchRequests(request)) { + String method = batchRequest.getMethod(); + String pathInfo = batchRequest.getRelativeUrl(); + if (isApiMatching(method, pathInfo)) { + return true; + } + } + return false; + } + + private List getBatchRequests(BodyCachingHttpServletRequestWrapper request) throws IOException { + List batchRequests = objectMapper.readValue(request.getInputStream(), new TypeReference<>() {}); + // since we read body, we have to reset so the upcoming readings are successful + request.resetStream(); + for (BatchRequest batchRequest : batchRequests) { + String pathInfo = "/" + batchRequest.getRelativeUrl(); + if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) { + pathInfo = "/v1/" + batchRequest.getRelativeUrl(); + } + batchRequest.setRelativeUrl(pathInfo); + } + return batchRequests; + } + + private boolean isApiMatching(String method, String pathInfo) { + return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find() + && URL_FUNCTION.test(pathInfo); + } + + private boolean isBatchApi(String pathInfo) { + return pathInfo.startsWith("/v1/batches"); + } + + private boolean isGlim(String pathInfo) { + return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches(); + } + + private List getGlimChildLoanIds(Long loanIdFromRequest) { + GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true, + BigDecimal.valueOf(loanIdFromRequest)); + if (glimAccount != null) { + return glimAccount.getChildLoan().stream().map(Loan::getId).toList(); + } else { + return Collections.emptyList(); + } + } + + public List calculateRelevantLoanIds(BodyCachingHttpServletRequestWrapper request) throws IOException { + String pathInfo = request.getPathInfo(); + if (isBatchApi(pathInfo)) { + return getLoanIdsFromBatchApi(request); + } else { + return getLoanIdsFromApi(pathInfo); + } + } + + private List getLoanIdsFromBatchApi(BodyCachingHttpServletRequestWrapper request) throws IOException { + List loanIds = new ArrayList<>(); + for (BatchRequest batchRequest : getBatchRequests(request)) { + // check the URL for Loan related ID + String relativeUrl = batchRequest.getRelativeUrl(); + if (!relativeUrl.contains("$.resourceId")) { + // if resourceId reference is used, we simply don't know the resourceId without executing the requests + // first, so skipping it + loanIds.addAll(getLoanIdsFromApi(relativeUrl)); + } + + // check the body for Loan ID + Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest); + if (loanId != null) { + loanIds.add(loanId); + } + } + return loanIds; + } + + private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws JsonProcessingException { + String body = batchRequest.getBody(); + if (StringUtils.isNotBlank(body)) { + JsonNode jsonNode = objectMapper.readTree(body); + if (jsonNode.has("loanId")) { + return jsonNode.get("loanId").asLong(); + } + } + return null; + } + + private List getLoanIdsFromApi(String pathInfo) { + return getLoanIdList(pathInfo); + } + + private List getLoanIdList(String pathInfo) { + boolean isGlim = isGlim(pathInfo); + Long loanIdFromRequest = getLoanId(isGlim, pathInfo); + if (loanIdFromRequest == null) { + return Collections.emptyList(); + } + if (isGlim) { + return getGlimChildLoanIds(loanIdFromRequest); + } else { + return Collections.singletonList(loanIdFromRequest); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + objectMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java index 4446972e8c6..ce70a02cd96 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java @@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; +import org.apache.fineract.infrastructure.jobs.filter.ProgressiveLoanModelCheckerFilter; import org.apache.fineract.infrastructure.security.converter.FineractJwtAuthenticationTokenConverter; import org.apache.fineract.infrastructure.security.data.TenantAuthenticationDetails; import org.apache.fineract.infrastructure.security.filter.BusinessDateFilter; @@ -118,6 +119,9 @@ public class AuthorizationServerConfig { @Autowired private BusinessDateReadPlatformService businessDateReadPlatformService; + @Autowired + ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter; + @Bean @Order(1) public SecurityFilterChain publicEndpoints(HttpSecurity http) throws Exception { @@ -173,9 +177,12 @@ public SecurityFilterChain protectedEndpoints(HttpSecurity http) throws Exceptio if (!Objects.isNull(loanCOBFilterHelper)) { http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) // .addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); // + http.addFilterBefore(progressiveLoanModelCheckerFilter, LoanCOBApiFilter.class); } else { http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); // + http.addFilterAfter(progressiveLoanModelCheckerFilter, FineractInstanceModeApiFilter.class); } + if (fineractProperties.getIpTracking().isEnabled()) { http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java index a5b19982a23..8625b498a2a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java @@ -33,6 +33,7 @@ import java.util.TreeMap; import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.StepExecution; import org.springframework.batch.item.ExecutionContext; @@ -41,8 +42,9 @@ public class LoanItemProcessorStepDefinitions implements En { private COBBusinessStepService cobBusinessStepService = mock(COBBusinessStepService.class); + private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService = mock(ProgressiveLoanModelProcessingService.class); - private LoanItemProcessor loanItemProcessor = new LoanItemProcessor(cobBusinessStepService); + private LoanItemProcessor loanItemProcessor = new LoanItemProcessor(cobBusinessStepService, progressiveLoanModelProcessingService); private Loan loan = mock(Loan.class); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java index 39781556280..5b6e7f748f1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java @@ -202,15 +202,15 @@ public static BatchRequest createClientRequest(final Long requestId, final Strin br.setMethod("POST"); final String extId; - if (externalId.equals("")) { - extId = "ext" + String.valueOf((10000 * secureRandom.nextDouble())) + String.valueOf((10000 * secureRandom.nextDouble())); + if (externalId.isEmpty()) { + extId = UUID.randomUUID().toString(); } else { extId = externalId; } - final String body = "{ \"officeId\": 1, \"legalFormId\":1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + "\"externalId\": " - + extId + ", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\"," - + "\"active\": false, \"submittedOnDate\": \"04 March 2009\"}"; + final String body = "{ \"officeId\": 1, \"legalFormId\":1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + "\"externalId\": \"" + + extId + + "\", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\",\"active\": false, \"submittedOnDate\": \"04 March 2009\"}"; br.setBody(body);