FINERACT-2421: Allow ProgressiveLoanInterestScheduleModel recalculation
Co-authored-by: Soma Sörös <soma.soros@dpc.hu>
This commit is contained in:
+7
@@ -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 |
|
||||||
|
|||||||
+6
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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>
|
||||||
|
|||||||
+35
@@ -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>
|
||||||
+13
-2
@@ -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));
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
|
|||||||
+5
-1
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
|
|||||||
+5
@@ -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);
|
||||||
|
|||||||
+80
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+217
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+7
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user