diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java index 92e4493c2e7..e5d611bea59 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java @@ -555,6 +555,44 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobErr log.debug("ERROR MESSAGE: {}", errorMessageActual); } + @When("Batch API call with created user and the following data results a {int} statuscode WITHOUT error message:") + public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobWithoutError(int httpCodeExpected, DataTable table) + throws IOException { + String idempotencyKey = UUID.randomUUID().toString(); + Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long loanId = loanResponse.body().getLoanId(); + + List> data = table.asLists(); + List transferData = data.get(1); + String fromDateStr = transferData.get(0); + String submittedOnDate = transferData.get(1); + String toDateStr = transferData.get(2); + String approvedOnDate = transferData.get(3); + String enclosingTransaction = transferData.get(4); + + Map headerMap = new HashMap<>(); + + Response createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE); + Long createdUserId = createUserResponse.body().getResourceId(); + Response user = usersApi.retrieveOne31(createdUserId).execute(); + ErrorHelper.checkSuccessfulApiCall(user); + String authorizationString = user.body().getUsername() + ":" + PWD_USER_WITH_ROLE; + Base64 base64 = new Base64(); + headerMap.put("Authorization", + "Basic " + new String(base64.encode(authorizationString.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)); + List requestList = new ArrayList<>(); + requestList.add(createLoanReschedule(1L, loanId, fromDateStr, toDateStr, submittedOnDate, idempotencyKey, null)); + requestList.add(approveLoanReschedule(2L, idempotencyKey, approvedOnDate, 1L)); + + Boolean isEnclosingTransaction = Boolean.valueOf(enclosingTransaction); + Response> batchResponseList = batchApiApi.handleBatchRequests(requestList, isEnclosingTransaction, headerMap) + .execute(); + BatchResponse lastBatchResponse = batchResponseList.body().get(batchResponseList.body().size() - 1); + assertThat(httpCodeExpected).isEqualTo(lastBatchResponse.getStatusCode()); + // No error + assertThat(batchResponseList.errorBody()).isEqualTo(null); + } + @When("Batch API call with steps: queryDatatable, updateDatatable runs, with empty queryDatatable response") public void runBatchApiQueryDatatableUpdateDatatable() throws IOException { String idempotencyKey = UUID.randomUUID().toString(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature index 462b51f7d40..c8f3d57fc65 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature @@ -659,7 +659,7 @@ Feature: LoanReschedule Then Admin checks that last closed business date of loan is "09 January 2024" @TestRailId:C3048 @AdvancedPaymentAllocation - Scenario: Verify that in case of Loan is hard locked for COB execution, BatchAPI request of Loan reschedule creation and approval will result a 409 error and a LOAN_LOCKED_BY_COB error message + Scenario: Verify that in case of Loan is hard locked for COB execution without error message, BatchAPI request of Loan reschedule creation and approval will result a 409 error and a LOAN_LOCKED_BY_COB error message When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -671,7 +671,7 @@ Feature: LoanReschedule When Admin sets the business date to "02 January 2024" When Admin runs inline COB job for Loan When Admin sets the business date to "10 January 2024" - When Admin places a lock on loan account with an error message + When Admin places a lock on loan account WITHOUT an error message When Admin creates new user with "NO_BYPASS_AUTOTEST" username, "NO_BYPASS_AUTOTEST_ROLE" role name and given permissions: | APPROVE_RESCHEDULELOAN | | CREATE_RESCHEDULELOAN | @@ -680,3 +680,26 @@ Feature: LoanReschedule When Batch API call with created user and the following data results a 409 error and a "LOAN_LOCKED_BY_COB" error message: | rescheduleFromDate | submittedOnDate | adjustedDueDate | approvedOnDate | enclosingTransaction | | 16 January 2024 | 10 January 2024 | 31 January 2024 | 10 January 2024 | true | + + @TestRailId:C3049 @AdvancedPaymentAllocation + Scenario: Verify that in case of Loan is hard locked for COB execution with error message, BatchAPI request of Loan reschedule creation and approval will result a 200 statuscode without error message + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + When Admin sets the business date to "02 January 2024" + When Admin runs inline COB job for Loan + When Admin sets the business date to "10 January 2024" + When Admin places a lock on loan account with an error message + When Admin creates new user with "NO_BYPASS_AUTOTEST" username, "NO_BYPASS_AUTOTEST_ROLE" role name and given permissions: + | APPROVE_RESCHEDULELOAN | + | CREATE_RESCHEDULELOAN | + | READ_RESCHEDULELOAN | + | REJECT_RESCHEDULELOAN | + When Batch API call with created user and the following data results a 200 statuscode WITHOUT error message: + | rescheduleFromDate | submittedOnDate | adjustedDueDate | approvedOnDate | enclosingTransaction | + | 16 January 2024 | 10 January 2024 | 31 January 2024 | 10 January 2024 | true | 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"));