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);