This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 6269cbbb79 FINERACT-2421: Allow ProgressiveLoanInterestScheduleModel 
recalculation
6269cbbb79 is described below

commit 6269cbbb79022171def3623acf79d4043723e81d
Author: Adam Saghy <[email protected]>
AuthorDate: Thu Feb 12 22:17:15 2026 +0100

    FINERACT-2421: Allow ProgressiveLoanInterestScheduleModel recalculation
    
    Co-authored-by: Soma Sörös <[email protected]>
---
 .../fineract/test/stepdef/loan/LoanStepDef.java    |   7 +
 .../features/LoanAccrualTransaction.feature        |   2 +
 .../loanaccount/domain/LoanRepository.java         |   6 +
 .../loanaccount/domain/LoanRepositoryWrapper.java  |   4 +
 ...EmbeddableProgressiveLoanScheduleGenerator.java |   5 +
 .../loanaccount/domain/ProgressiveLoanModel.java   |   3 +
 .../repository/ProgressiveLoanModelRepository.java |   7 +
 .../InterestScheduleModelRepositoryWrapper.java    |   2 +
 ...InterestScheduleModelRepositoryWrapperImpl.java |   7 +
 .../InternalProgressiveLoanApiResource.java        |  10 +
 .../ProgressiveLoanModelProcessingService.java     |  61 ++++++
 .../ProgressiveLoanModelRecalculationService.java  |  61 ++++++
 .../data/ProgressiveLoanInterestScheduleModel.java |   5 +
 .../progressiveloan/module-changelog-master.xml    |   1 +
 .../5003_add_model_version.xml}                    |  19 +-
 .../cob/loan/AbstractLoanItemProcessor.java        |  15 +-
 .../cob/loan/InlineCOBLoanItemProcessor.java       |   6 +-
 .../cob/loan/LoanCOBWorkerConfiguration.java       |   6 +-
 .../fineract/cob/loan/LoanInlineCOBConfig.java     |   6 +-
 .../fineract/cob/loan/LoanItemProcessor.java       |   6 +-
 .../infrastructure/core/config/SecurityConfig.java |   5 +
 .../filter/ProgressiveLoanModelCheckerFilter.java  |  80 ++++++++
 .../filter/ProgressiveLoanModelCheckerHelper.java  | 217 +++++++++++++++++++++
 .../security/config/AuthorizationServerConfig.java |   7 +
 .../cob/loan/LoanItemProcessorStepDefinitions.java |   4 +-
 .../integrationtests/common/BatchHelper.java       |  10 +-
 26 files changed, 541 insertions(+), 21 deletions(-)

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 9d7d26e098..23dcb365d3 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 class LoanStepDef extends AbstractStepDef {
         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> 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 3c831869dd..df4c814e6a 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 0cb93c1038..05c98ee926 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 @@ public interface LoanRepository extends 
JpaRepository<Loan, Long>, JpaSpecificat
     @Query(FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE)
     List<COBIdAndLastClosedBusinessDate> 
findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate 
cobBusinessDate,
             @Param("loanIds") List<Long> loanIds, @Param("loanStatuses") 
Collection<LoanStatus> 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<LoanStatus> 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 7943c95715..66241087fb 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 class LoanRepositoryWrapper {
     public boolean isEnabledCapitalizedIncome(Long loanId) {
         return repository.isEnabledCapitalizedIncome(loanId);
     }
+
+    public boolean isLoanInAllowedStatus(Long loanId, List<LoanStatus> 
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 864a20c3e1..79f97b7566 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 class EmbeddableProgressiveLoanScheduleGenerator {
         public Optional<ProgressiveLoanInterestScheduleModel> 
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 c8aec9b2fd..5ac311c860 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<Long> {
     @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 be6af1aac2..4106600205 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.Loan;
 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<ProgressiveLoanModel>, 
JpaRepository<ProgressiveLoanModel, Long> {
@@ -30,4 +32,9 @@ public interface ProgressiveLoanModelRepository
     Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
 
     Optional<ProgressiveLoanModel> 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 1ce74910b7..977fa53dc1 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 @@ public interface InterestScheduleModelRepositoryWrapper {
     boolean hasValidModelForDate(Long loanId, LocalDate targetDate);
 
     Optional<ProgressiveLoanInterestScheduleModel> 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 654af32509..97683efefc 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 class InterestScheduleModelRepositoryWrapperImpl 
implements InterestSched
             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 class InterestScheduleModelRepositoryWrapperImpl 
implements InterestSched
                 .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 4fca8a38f8..ad05ecf43a 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 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 class InternalProgressiveLoanApiResource implements 
InitializingBean {
 
         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 0000000000..4cedc8a6cc
--- /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<LoanStatus> 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 0000000000..66e41f573c
--- /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<LoanTransaction> loanTransactions = 
loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
+            Pair<ChangedTransactionDetail, 
ProgressiveLoanInterestScheduleModel> 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 55d9f000bf..c479d6d3a1 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 @@ import 
org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetail
 @AllArgsConstructor
 public class ProgressiveLoanInterestScheduleModel {
 
+    private static final String modelVersion = "2";
     private final List<RepaymentPeriod> repaymentPeriods;
     private final TreeSet<InterestRate> interestRates;
     @JsonExclude
@@ -419,4 +420,8 @@ public class ProgressiveLoanInterestScheduleModel {
         };
     }
 
+    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 c0f34fbf24..a18949ec16 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 @@
     <!-- Sequence is starting from 5000 to make it easier to move existing 
liquibase changesets here -->
     <include file="parts/5001_create_progressive_loan_model.xml" 
relativeToChangelogFile="true"/>
     <include file="parts/5002_add_contract_termination_transaction.xml" 
relativeToChangelogFile="true"/>
+    <include file="parts/5003_add_model_version.xml" 
relativeToChangelogFile="true"/>
 </databaseChangeLog>
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/parts/5003_add_model_version.xml
similarity index 57%
copy from 
fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml
copy to 
fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5003_add_model_version.xml
index c0f34fbf24..cb601b8c61 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/parts/5003_add_model_version.xml
@@ -19,10 +19,17 @@
     under the License.
 
 -->
-<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
-                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
-    <!-- Sequence is starting from 5000 to make it easier to move existing 
liquibase changesets here -->
-    <include file="parts/5001_create_progressive_loan_model.xml" 
relativeToChangelogFile="true"/>
-    <include file="parts/5002_add_contract_termination_transaction.xml" 
relativeToChangelogFile="true"/>
+<databaseChangeLog
+        xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+                      
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.31.xsd";
+        objectQuotingStrategy="QUOTE_ONLY_RESERVED_WORDS">
+    <changeSet author="fineract" id="1">
+        <addColumn tableName="m_loan_progressive_model">
+            <column name="json_model_version" type="VARCHAR(100)" 
defaultValue="1">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
 </databaseChangeLog>
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 b5c7f544c2..42f140b3a6 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 lombok.extern.slf4j.Slf4j;
 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 @@ import org.springframework.lang.NonNull;
 public abstract class AbstractLoanItemProcessor implements ItemProcessor<Loan, 
Loan> {
 
     private final COBBusinessStepService cobBusinessStepService;
+    private final ProgressiveLoanModelProcessingService 
progressiveLoanModelProcessingService;
 
     @Setter(AccessLevel.PROTECTED)
     private ExecutionContext executionContext;
@@ -51,18 +54,26 @@ public abstract class AbstractLoanItemProcessor implements 
ItemProcessor<Loan, L
 
     @SuppressWarnings({ "unchecked" })
     @Override
-    public Loan process(@NonNull Loan item) throws Exception {
+    public Loan process(@NonNull Loan loan) throws Exception {
+        if (needToRebuildModel(loan)) {
+            
progressiveLoanModelProcessingService.recalculateModelAndSave(loan.getId());
+        }
         Set<BusinessStepNameAndOrder> businessSteps = 
(Set<BusinessStepNameAndOrder>) 
executionContext.get(LoanCOBConstant.BUSINESS_STEPS);
         if (businessSteps == null) {
             throw new IllegalStateException("No business steps found in the 
execution context");
         }
         TreeMap<Long, String> 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<Long, String> 
getBusinessStepMap(Set<BusinessStepNameAndOrder> businessSteps) {
         Map<Long, String> 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 75894dd4cc..4326d84d98 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 f2e9bc4303..17d97f5709 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.jobs.service.JobName;
 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 class LoanCOBWorkerConfiguration {
     @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 343c074ed3..bd00a20ad3 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.jobs.service.JobName;
 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 class LoanInlineCOBConfig {
     @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 2aa95d27b4..4e7c340d5c 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 438923e6f4..f510cef425 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.core.service.MDCWrapper;
 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 class SecurityConfig {
         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 0000000000..a47a7ae4c5
--- /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<Long> 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 0000000000..c6793fb6da
--- /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<HttpMethod> 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<String> 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<BatchRequest> 
getBatchRequests(BodyCachingHttpServletRequestWrapper request) throws 
IOException {
+        List<BatchRequest> 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<Long> 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<Long> 
calculateRelevantLoanIds(BodyCachingHttpServletRequestWrapper request) throws 
IOException {
+        String pathInfo = request.getPathInfo();
+        if (isBatchApi(pathInfo)) {
+            return getLoanIdsFromBatchApi(request);
+        } else {
+            return getLoanIdsFromApi(pathInfo);
+        }
+    }
+
+    private List<Long> 
getLoanIdsFromBatchApi(BodyCachingHttpServletRequestWrapper request) throws 
IOException {
+        List<Long> 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<Long> getLoanIdsFromApi(String pathInfo) {
+        return getLoanIdList(pathInfo);
+    }
+
+    private List<Long> 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 4446972e8c..ce70a02cd9 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.core.service.MDCWrapper;
 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 class AuthorizationServerConfig {
         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 a5b19982a2..8625b498a2 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.Collections;
 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 @@ import org.springframework.batch.item.ExecutionContext;
 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 3978155628..5b6e7f748f 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 final class BatchHelper {
         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);
 

Reply via email to