From 2ad4eb917ace80399d3a3bd5383011f2dd0360d2 Mon Sep 17 00:00:00 2001 From: Ashhar Ahmad Khan <145142826+AshharAhmadKhan@users.noreply.github.com> Date: Fri, 15 May 2026 23:56:08 +0530 Subject: [PATCH] FINERACT-2602: Remove stuck loans from loan lock table after COB execution --- .../cob/domain/LoanAccountLockRepository.java | 3 + .../cob/loan/LoanCOBManagerConfiguration.java | 15 ++++ .../cob/loan/UnlockProcessedLoansTasklet.java | 47 ++++++++++++ ...kProcessedLoansTaskletStepDefinitions.java | 74 +++++++++++++++++++ .../cob.loan.unlockprocessed.step.feature | 24 ++++++ 5 files changed, 163 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTasklet.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTaskletStepDefinitions.java create mode 100644 fineract-provider/src/test/resources/features/cob/loan/cob.loan.unlockprocessed.step.feature 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 3724185c46f..9b7509a5f42 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 @@ -18,6 +18,7 @@ */ package org.apache.fineract.cob.domain; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -49,4 +50,6 @@ public interface LoanAccountLockRepository @Override void removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(List lockOwners); + void deleteByLockOwnerAndErrorIsNullAndLockPlacedOnCobBusinessDate(LockOwner lockOwner, LocalDate cobBusinessDate); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java index c81e92a44e5..265652a44a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java @@ -23,6 +23,7 @@ import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.cob.common.CustomJobParameterResolver; import org.apache.fineract.cob.conditions.BatchManagerCondition; +import org.apache.fineract.cob.domain.LoanAccountLockRepository; import org.apache.fineract.cob.listener.COBExecutionListenerRunner; import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; @@ -76,6 +77,8 @@ public class LoanCOBManagerConfiguration { private BusinessEventNotifierService businessEventNotifierService; @Autowired private CustomJobParameterResolver customJobParameterResolver; + @Autowired + private LoanAccountLockRepository loanAccountLockRepository; @Bean @StepScope @@ -103,6 +106,12 @@ public Step stayedLockedStep() { .build(); } + @Bean + public Step unlockProcessedLoansStep() { + return new StepBuilder("Unlock processed loans - Step", jobRepository).tasklet(unlockProcessedLoansTasklet(), transactionManager) + .build(); + } + @Bean public ResolveLoanCOBCustomJobParametersTasklet resolveCustomJobParametersTasklet() { return new ResolveLoanCOBCustomJobParametersTasklet(customJobParameterResolver); @@ -113,6 +122,11 @@ public StayedLockedLoansTasklet stayedLockedTasklet() { return new StayedLockedLoansTasklet(businessEventNotifierService, retrieveIdService); } + @Bean + public UnlockProcessedLoansTasklet unlockProcessedLoansTasklet() { + return new UnlockProcessedLoansTasklet(loanAccountLockRepository); + } + @Bean(name = "loanCOBJob") public Job loanCOBJob(LoanCOBPartitioner partitioner) { return new JobBuilder(JobName.LOAN_COB.name(), jobRepository) // @@ -120,6 +134,7 @@ public Job loanCOBJob(LoanCOBPartitioner partitioner) { .start(resolveCustomJobParametersStep()) // .next(loanCOBStep(partitioner)) // .next(stayedLockedStep()) // + .next(unlockProcessedLoansStep()) // .incrementer(new RunIdIncrementer()) // .build(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTasklet.java new file mode 100644 index 00000000000..c294ca6d2b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTasklet.java @@ -0,0 +1,47 @@ +/** + * 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.cob.loan; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.LoanAccountLockRepository; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +@Slf4j +@RequiredArgsConstructor +public class UnlockProcessedLoansTasklet implements Tasklet { + + private final LoanAccountLockRepository loanAccountLockRepository; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); + log.debug("Removing orphaned locks for successfully processed loans on COB date: {}", cobBusinessDate); + loanAccountLockRepository.deleteByLockOwnerAndErrorIsNullAndLockPlacedOnCobBusinessDate(LockOwner.LOAN_COB_CHUNK_PROCESSING, + cobBusinessDate); + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTaskletStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTaskletStepDefinitions.java new file mode 100644 index 00000000000..530a192f5ed --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/UnlockProcessedLoansTaskletStepDefinitions.java @@ -0,0 +1,74 @@ +/** + * 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.cob.loan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.cucumber.java8.En; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashMap; +import org.apache.fineract.cob.domain.LoanAccountLockRepository; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.mockito.Mockito; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.repeat.RepeatStatus; + +public class UnlockProcessedLoansTaskletStepDefinitions implements En { + + private LoanAccountLockRepository loanAccountLockRepository = mock(LoanAccountLockRepository.class); + private UnlockProcessedLoansTasklet tasklet = new UnlockProcessedLoansTasklet(loanAccountLockRepository); + private RepeatStatus resultItem; + private StepContribution stepContribution; + private LocalDate cobDate; + + public UnlockProcessedLoansTaskletStepDefinitions() { + Given("The UnlockProcessedLoansTasklet.execute method is called", () -> { + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + HashMap businessDateMap = new HashMap<>(); + cobDate = LocalDate.now(ZoneId.systemDefault()); + businessDateMap.put(BusinessDateType.COB_DATE, cobDate); + ThreadLocalContextUtil.setBusinessDates(businessDateMap); + JobExecution jobExecution = new JobExecution(1L, null); + StepExecution stepExecution = new StepExecution("test", jobExecution); + stepContribution = new StepContribution(stepExecution); + }); + + When("UnlockProcessedLoansTasklet.execute method executed", () -> { + try { + resultItem = tasklet.execute(stepContribution, null); + } finally { + ThreadLocalContextUtil.reset(); + } + }); + + Then("UnlockProcessedLoansTasklet.execute result should match", () -> { + assertEquals(RepeatStatus.FINISHED, resultItem); + verify(loanAccountLockRepository, Mockito.times(1)) + .deleteByLockOwnerAndErrorIsNullAndLockPlacedOnCobBusinessDate(LockOwner.LOAN_COB_CHUNK_PROCESSING, cobDate); + }); + } +} diff --git a/fineract-provider/src/test/resources/features/cob/loan/cob.loan.unlockprocessed.step.feature b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.unlockprocessed.step.feature new file mode 100644 index 00000000000..977fd7c7fb8 --- /dev/null +++ b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.unlockprocessed.step.feature @@ -0,0 +1,24 @@ +# +# 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. +# +Feature: COB Unlock Processed Loans Step + @cob + Scenario: UnlockProcessedLoansTasklet - run test + Given The UnlockProcessedLoansTasklet.execute method is called + When UnlockProcessedLoansTasklet.execute method executed + Then UnlockProcessedLoansTasklet.execute result should match