diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java index 46d6dc9dd59..08e908fad6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java @@ -36,6 +36,8 @@ public interface LoanAccountLockRepository boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + boolean existsByLoanIdAndLockOwnerAndErrorIsNotNull(Long loanId, LockOwner lockOwner); + @Query(""" delete from LoanAccountLock lck where lck.lockPlacedOnCobBusinessDate is not null and lck.error is not null and lck.lockOwner in (org.apache.fineract.cob.domain.LockOwner.LOAN_COB_CHUNK_PROCESSING,org.apache.fineract.cob.domain.LockOwner.LOAN_INLINE_COB_PROCESSING) diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java index 6d33f73ee6b..a2bc44816c5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java @@ -27,5 +27,7 @@ public interface LoanAccountLockService { boolean isLoanHardLocked(Long loanId); + boolean isLockOverrulable(Long loanId); + void updateCobAndRemoveLocks(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java index 2a7ef4837f4..1077c7afad0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java @@ -49,6 +49,12 @@ public boolean isLoanHardLocked(Long loanId) { || loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING); } + @Override + public boolean isLockOverrulable(Long loanId) { + return loanAccountLockRepository.existsByLoanIdAndLockOwnerAndErrorIsNotNull(loanId, LockOwner.LOAN_COB_CHUNK_PROCESSING) // + || loanAccountLockRepository.existsByLoanIdAndLockOwnerAndErrorIsNotNull(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING); + } + @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateCobAndRemoveLocks() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java index 40c0a2c426a..670caf59889 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java @@ -186,6 +186,14 @@ private boolean isLoanHardLocked(List loanIds) { return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked); } + private boolean isLockOverrulable(Long... loanIds) { + return isLockOverrulable(Arrays.asList(loanIds)); + } + + private boolean isLockOverrulable(List loanIds) { + return loanIds.stream().anyMatch(loanAccountLockService::isLockOverrulable); + } + public boolean isLoanBehind(List loanIds) { List loanIdAndLastClosedBusinessDates = new ArrayList<>(); List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit()); @@ -217,7 +225,7 @@ private List getLoanIdsFromBatchApi(BodyCachingHttpServletRequestWrapper r // check the body for Loan ID Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest); if (loanId != null) { - if (isLoanHardLocked(loanId)) { + if (isLoanHardLocked(loanId) && !isLockOverrulable(loanId)) { throw new LoanIdsHardLockedException(loanId); } else { loanIds.add(loanId); @@ -240,7 +248,7 @@ private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws private List getLoanIdsFromApi(String pathInfo) { List loanIds = getLoanIdList(pathInfo); - if (isLoanHardLocked(loanIds)) { + if (isLoanHardLocked(loanIds) && !isLockOverrulable(loanIds)) { throw new LoanIdsHardLockedException(loanIds.get(0)); } else { return loanIds; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 645ea67feb8..07bf0a4c774 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -80,6 +81,9 @@ import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.organisation.StaffHelper; +import org.apache.fineract.integrationtests.useradministration.roles.RolesHelper; +import org.apache.fineract.integrationtests.useradministration.users.UserHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; @@ -5709,6 +5713,73 @@ public void uc151() { } + @Test + public void uc152() { + AtomicLong createdLoanId = new AtomicLong(); + runAt("01 January 2024", () -> { + Long clientId = client.getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRateFrequencyType(YEARS).numberOfRepayments(4)// + .maxInterestRatePerPeriod((double) 0)// + .repaymentEvery(1)// + .repaymentFrequencyType(1L)// + .allowPartialPeriodInterestCalcualtion(false)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(null)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .installmentAmountInMultiplesOf(null)// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "01 January 2024", 400.0, + 6); + + applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS).interestRatePerPeriod(BigDecimal.ZERO) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(400.0)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2024").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2024").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(400.0)).locale("en")); + }); + + runAt("02 January 2024", () -> { + executeInlineCOB(createdLoanId.get()); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertEquals(LocalDate.of(2024, 1, 1), loanDetails.getLastClosedBusinessDate()); + final String errorMessage = Utils.uniqueRandomStringGenerator("error.", 40); + placeHardLockOnLoan(createdLoanId.get(), errorMessage); + }); + + runAt("03 January 2024", () -> { + Integer roleId = RolesHelper.createRole(requestSpec, responseSpec); + Map permissionMap = Map.of("REPAYMENT_LOAN", true); + RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap); + final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); + + final String operatorUser = Utils.uniqueRandomStringGenerator("user", 8); + UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, operatorUser, UserHelper.SIMPLE_USER_PASSWORD, + "resourceId"); + + loanTransactionHelper.makeLoanRepayment( + createdLoanId.get(), new PostLoansLoanIdTransactionsRequest().transactionDate("03 January 2024") + .dateFormat("dd MMMM yyyy").locale("en").transactionAmount(200.0), + operatorUser, UserHelper.SIMPLE_USER_PASSWORD); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertEquals(LocalDate.of(2024, 1, 2), loanDetails.getLastClosedBusinessDate()); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index bee30bc8c63..04f86dc387e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -600,6 +600,10 @@ protected void placeHardLockOnLoan(Long loanId) { loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING"); } + protected void placeHardLockOnLoan(Long loanId, String error) { + loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING", error); + } + protected void executeInlineCOB(Long loanId) { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index a8246150ac1..c00b137595c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -602,6 +602,11 @@ public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment")); } + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final PostLoansLoanIdTransactionsRequest request, + final String user, final String pass) { + return ok(newFineract(user, pass).loanTransactions.executeLoanTransaction(loanId, request, "repayment")); + } + public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));