FINERACT-2421: Allow ProgressiveLoanInterestScheduleModel recalculation

Co-authored-by: Soma Sörös <soma.soros@dpc.hu>
This commit is contained in:
Adam Saghy
2026-02-13 14:58:37 +01:00
co-authored by Soma Sörös
parent a8fb5e7826
commit 6269cbbb79
26 changed files with 563 additions and 15 deletions
@@ -5926,6 +5926,13 @@ public class LoanStepDef extends AbstractStepDef {
assertEquals(Integer.valueOf(numberOfRelations), relationshipOptional.size(), "Missed relationship for transaction"); 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, public static AdvancedPaymentData editPaymentAllocationFutureInstallment(String transactionType, String futureInstallmentAllocationRule,
List<PaymentAllocationOrder> paymentAllocationOrder) { List<PaymentAllocationOrder> paymentAllocationOrder) {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
@@ -1619,6 +1619,7 @@ Feature: LoanAccrualTransaction
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 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 --- # --- Early repayment with 17.01 EUR on 15 Jan ---
When Admin sets the business date to "15 January 2024" 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 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: 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 | | 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 | | 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 | | 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 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 And Admin runs inline COB job for Loan
Then Loan Repayment schedule has 6 periods, with the following data for periods: 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 | | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
@@ -279,4 +279,10 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
@Query(FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE) @Query(FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE)
List<COBIdAndLastClosedBusinessDate> findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate cobBusinessDate, List<COBIdAndLastClosedBusinessDate> findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate cobBusinessDate,
@Param("loanIds") List<Long> loanIds, @Param("loanStatuses") Collection<LoanStatus> loanStatuses); @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);
} }
@@ -313,4 +313,8 @@ public class LoanRepositoryWrapper {
public boolean isEnabledCapitalizedIncome(Long loanId) { public boolean isEnabledCapitalizedIncome(Long loanId) {
return repository.isEnabledCapitalizedIncome(loanId); return repository.isEnabledCapitalizedIncome(loanId);
} }
public boolean isLoanInAllowedStatus(Long loanId, List<LoanStatus> allowedLoanStatuses) {
return repository.isLoanInAllowedStatus(loanId, allowedLoanStatuses);
}
} }
@@ -83,5 +83,10 @@ public class EmbeddableProgressiveLoanScheduleGenerator {
public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate) { public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate) {
return Optional.empty(); return Optional.empty();
} }
@Override
public Long removeByLoanId(Long loanId) {
return 0L;
}
} }
} }
@@ -52,4 +52,7 @@ public class ProgressiveLoanModel extends AbstractPersistableCustom<Long> {
@Column(name = "last_modified_on_utc", nullable = false) @Column(name = "last_modified_on_utc", nullable = false)
private OffsetDateTime lastModifiedDate; private OffsetDateTime lastModifiedDate;
@Column(name = "json_model_version", nullable = false)
private String jsonModelVersion;
} }
@@ -23,6 +23,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ProgressiveLoanModelRepository public interface ProgressiveLoanModelRepository
extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> { extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
@@ -30,4 +32,9 @@ public interface ProgressiveLoanModelRepository
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId); Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
Optional<ProgressiveLoanModel> findOneByLoan(Loan loan); 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);
} }
@@ -41,4 +41,6 @@ public interface InterestScheduleModelRepositoryWrapper {
boolean hasValidModelForDate(Long loanId, LocalDate targetDate); boolean hasValidModelForDate(Long loanId, LocalDate targetDate);
Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate); Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate);
Long removeByLoanId(Long loanId);
} }
@@ -61,11 +61,13 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> { ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> {
ProgressiveLoanModel plm = new ProgressiveLoanModel(); ProgressiveLoanModel plm = new ProgressiveLoanModel();
plm.setLoan(loan); plm.setLoan(loan);
plm.setJsonModelVersion(ProgressiveLoanInterestScheduleModel.getModelVersion());
return plm; return plm;
}); });
progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate());
progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime());
progressiveLoanModel.setJsonModel(jsonModel); progressiveLoanModel.setJsonModel(jsonModel);
progressiveLoanModel.setJsonModelVersion(ProgressiveLoanInterestScheduleModel.getModelVersion());
loanModelRepository.save(progressiveLoanModel); loanModelRepository.save(progressiveLoanModel);
}); });
return model; return model;
@@ -133,4 +135,9 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
.map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, detail, .map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, detail,
MoneyHelper.getMathContext(), installmentAmountInMultipliesOf)); // MoneyHelper.getMathContext(), installmentAmountInMultipliesOf)); //
} }
@Override
public Long removeByLoanId(Long loanId) {
return loanModelRepository.removeByLoanId(loanId);
}
} }
@@ -22,6 +22,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
@@ -99,4 +100,13 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
return writePlatformService.writeInterestScheduleModel(loan, model); 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);
}
} }
@@ -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);
}
}
@@ -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;
}
}
@@ -54,6 +54,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetail
@AllArgsConstructor @AllArgsConstructor
public class ProgressiveLoanInterestScheduleModel { public class ProgressiveLoanInterestScheduleModel {
private static final String modelVersion = "2";
private final List<RepaymentPeriod> repaymentPeriods; private final List<RepaymentPeriod> repaymentPeriods;
private final TreeSet<InterestRate> interestRates; private final TreeSet<InterestRate> interestRates;
@JsonExclude @JsonExclude
@@ -419,4 +420,8 @@ public class ProgressiveLoanInterestScheduleModel {
}; };
} }
public static String getModelVersion() {
return modelVersion;
}
} }
@@ -25,4 +25,5 @@
<!-- Sequence is starting from 5000 to make it easier to move existing liquibase changesets here --> <!-- 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/5001_create_progressive_loan_model.xml" relativeToChangelogFile="true"/>
<include file="parts/5002_add_contract_termination_transaction.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> </databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<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>
@@ -32,6 +32,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.cob.data.BusinessStepNameAndOrder; import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.ExitStatus;
import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep; import org.springframework.batch.core.annotation.AfterStep;
@@ -44,6 +46,7 @@ import org.springframework.lang.NonNull;
public abstract class AbstractLoanItemProcessor implements ItemProcessor<Loan, Loan> { public abstract class AbstractLoanItemProcessor implements ItemProcessor<Loan, Loan> {
private final COBBusinessStepService cobBusinessStepService; private final COBBusinessStepService cobBusinessStepService;
private final ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
@Setter(AccessLevel.PROTECTED) @Setter(AccessLevel.PROTECTED)
private ExecutionContext executionContext; private ExecutionContext executionContext;
@@ -51,18 +54,26 @@ public abstract class AbstractLoanItemProcessor implements ItemProcessor<Loan, L
@SuppressWarnings({ "unchecked" }) @SuppressWarnings({ "unchecked" })
@Override @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); Set<BusinessStepNameAndOrder> businessSteps = (Set<BusinessStepNameAndOrder>) executionContext.get(LoanCOBConstant.BUSINESS_STEPS);
if (businessSteps == null) { if (businessSteps == null) {
throw new IllegalStateException("No business steps found in the execution context"); throw new IllegalStateException("No business steps found in the execution context");
} }
TreeMap<Long, String> businessStepMap = getBusinessStepMap(businessSteps); TreeMap<Long, String> businessStepMap = getBusinessStepMap(businessSteps);
Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, item); Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, loan);
alreadyProcessedLoan.setLastClosedBusinessDate(businessDate); alreadyProcessedLoan.setLastClosedBusinessDate(businessDate);
return alreadyProcessedLoan; return alreadyProcessedLoan;
} }
private boolean needToRebuildModel(Loan loan) {
return loan.isProgressiveSchedule() && !progressiveLoanModelProcessingService.hasValidModel(loan.getId(),
ProgressiveLoanInterestScheduleModel.getModelVersion());
}
private TreeMap<Long, String> getBusinessStepMap(Set<BusinessStepNameAndOrder> businessSteps) { private TreeMap<Long, String> getBusinessStepMap(Set<BusinessStepNameAndOrder> businessSteps) {
Map<Long, String> businessStepMap = businessSteps.stream() Map<Long, String> businessStepMap = businessSteps.stream()
.collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName)); .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName));
@@ -19,13 +19,15 @@
package org.apache.fineract.cob.loan; package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.COBBusinessStepService; 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.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep; import org.springframework.batch.core.annotation.BeforeStep;
public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor { public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor {
public InlineCOBLoanItemProcessor(COBBusinessStepService cobBusinessStepService) { public InlineCOBLoanItemProcessor(COBBusinessStepService cobBusinessStepService,
super(cobBusinessStepService); ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) {
super(cobBusinessStepService, progressiveLoanModelProcessingService);
} }
@BeforeStep @BeforeStep
@@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.apache.fineract.infrastructure.springbatch.PropertyService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; 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.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.configuration.annotation.StepScope;
@@ -80,6 +81,9 @@ public class LoanCOBWorkerConfiguration {
@Autowired @Autowired
private LoanLockingService loanLockingService; private LoanLockingService loanLockingService;
@Autowired
private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
@Bean(name = LoanCOBConstant.LOAN_COB_WORKER_STEP) @Bean(name = LoanCOBConstant.LOAN_COB_WORKER_STEP)
public Step loanCOBWorkerStep(Flow cobFlow) { public Step loanCOBWorkerStep(Flow cobFlow) {
return stepBuilderFactory.get("Loan COB worker - Step").inputChannel(inboundRequests).flow(cobFlow).build(); return stepBuilderFactory.get("Loan COB worker - Step").inputChannel(inboundRequests).flow(cobFlow).build();
@@ -178,7 +182,7 @@ public class LoanCOBWorkerConfiguration {
@Bean @Bean
@StepScope @StepScope
public LoanItemProcessor cobWorkerItemProcessor() { public LoanItemProcessor cobWorkerItemProcessor() {
return new LoanItemProcessor(cobBusinessStepService); return new LoanItemProcessor(cobBusinessStepService, progressiveLoanModelProcessingService);
} }
@Bean @Bean
@@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.apache.fineract.infrastructure.springbatch.PropertyService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; 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.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope; import org.springframework.batch.core.configuration.annotation.JobScope;
@@ -65,9 +66,10 @@ public class LoanInlineCOBConfig {
private CustomJobParameterRepository customJobParameterRepository; private CustomJobParameterRepository customJobParameterRepository;
@Autowired @Autowired
private CustomJobParameterResolver customJobParameterResolver; private CustomJobParameterResolver customJobParameterResolver;
@Autowired @Autowired
private LoanLockingService loanLockingService; private LoanLockingService loanLockingService;
@Autowired
private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
@Bean @Bean
public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() { public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() {
@@ -106,7 +108,7 @@ public class LoanInlineCOBConfig {
@JobScope @JobScope
@Bean @Bean
public InlineCOBLoanItemProcessor inlineCobWorkerItemProcessor() { public InlineCOBLoanItemProcessor inlineCobWorkerItemProcessor() {
return new InlineCOBLoanItemProcessor(cobBusinessStepService); return new InlineCOBLoanItemProcessor(cobBusinessStepService, progressiveLoanModelProcessingService);
} }
@Bean @Bean
@@ -19,13 +19,15 @@
package org.apache.fineract.cob.loan; package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.COBBusinessStepService; 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.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep; import org.springframework.batch.core.annotation.BeforeStep;
public class LoanItemProcessor extends AbstractLoanItemProcessor { public class LoanItemProcessor extends AbstractLoanItemProcessor {
public LoanItemProcessor(COBBusinessStepService cobBusinessStepService) { public LoanItemProcessor(COBBusinessStepService cobBusinessStepService,
super(cobBusinessStepService); ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) {
super(cobBusinessStepService, progressiveLoanModelProcessingService);
} }
@BeforeStep @BeforeStep
@@ -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.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; 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.data.PlatformRequestLog;
import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter; import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter; import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
@@ -113,6 +114,8 @@ public class SecurityConfig {
private LoanCOBFilterHelper loanCOBFilterHelper; private LoanCOBFilterHelper loanCOBFilterHelper;
@Autowired @Autowired
private IdempotencyStoreHelper idempotencyStoreHelper; private IdempotencyStoreHelper idempotencyStoreHelper;
@Autowired
ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -216,8 +219,10 @@ public class SecurityConfig {
if (loanCOBFilterHelper != null) { if (loanCOBFilterHelper != null) {
http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class).addFilterAfter(idempotencyStoreFilter(), http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class).addFilterAfter(idempotencyStoreFilter(),
LoanCOBApiFilter.class); LoanCOBApiFilter.class);
http.addFilterBefore(progressiveLoanModelCheckerFilter, LoanCOBApiFilter.class);
} else { } else {
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class);
http.addFilterAfter(progressiveLoanModelCheckerFilter, FineractInstanceModeApiFilter.class);
} }
if (fineractProperties.getIpTracking().isEnabled()) { if (fineractProperties.getIpTracking().isEnabled()) {
http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class);
@@ -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);
}
}
@@ -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);
}
}
@@ -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.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; 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.converter.FineractJwtAuthenticationTokenConverter;
import org.apache.fineract.infrastructure.security.data.TenantAuthenticationDetails; import org.apache.fineract.infrastructure.security.data.TenantAuthenticationDetails;
import org.apache.fineract.infrastructure.security.filter.BusinessDateFilter; import org.apache.fineract.infrastructure.security.filter.BusinessDateFilter;
@@ -118,6 +119,9 @@ public class AuthorizationServerConfig {
@Autowired @Autowired
private BusinessDateReadPlatformService businessDateReadPlatformService; private BusinessDateReadPlatformService businessDateReadPlatformService;
@Autowired
ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter;
@Bean @Bean
@Order(1) @Order(1)
public SecurityFilterChain publicEndpoints(HttpSecurity http) throws Exception { public SecurityFilterChain publicEndpoints(HttpSecurity http) throws Exception {
@@ -173,9 +177,12 @@ public class AuthorizationServerConfig {
if (!Objects.isNull(loanCOBFilterHelper)) { if (!Objects.isNull(loanCOBFilterHelper)) {
http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) // http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); // .addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
http.addFilterBefore(progressiveLoanModelCheckerFilter, LoanCOBApiFilter.class);
} else { } else {
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); // http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); //
http.addFilterAfter(progressiveLoanModelCheckerFilter, FineractInstanceModeApiFilter.class);
} }
if (fineractProperties.getIpTracking().isEnabled()) { if (fineractProperties.getIpTracking().isEnabled()) {
http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class);
} }
@@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.TreeMap; import java.util.TreeMap;
import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.JobExecution;
import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ExecutionContext;
@@ -41,8 +42,9 @@ import org.springframework.batch.item.ExecutionContext;
public class LoanItemProcessorStepDefinitions implements En { public class LoanItemProcessorStepDefinitions implements En {
private COBBusinessStepService cobBusinessStepService = mock(COBBusinessStepService.class); 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); private Loan loan = mock(Loan.class);
@@ -202,15 +202,15 @@ public final class BatchHelper {
br.setMethod("POST"); br.setMethod("POST");
final String extId; final String extId;
if (externalId.equals("")) { if (externalId.isEmpty()) {
extId = "ext" + String.valueOf((10000 * secureRandom.nextDouble())) + String.valueOf((10000 * secureRandom.nextDouble())); extId = UUID.randomUUID().toString();
} else { } else {
extId = externalId; extId = externalId;
} }
final String body = "{ \"officeId\": 1, \"legalFormId\":1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + "\"externalId\": " final String body = "{ \"officeId\": 1, \"legalFormId\":1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + "\"externalId\": \""
+ extId + ", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\"," + extId
+ "\"active\": false, \"submittedOnDate\": \"04 March 2009\"}"; + "\", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\",\"active\": false, \"submittedOnDate\": \"04 March 2009\"}";
br.setBody(body); br.setBody(body);