diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java b/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java index 0e874850aac..6dce32c04dd 100644 --- a/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java @@ -27,7 +27,10 @@ public class COBConstant { public static final String COB_CUSTOM_JOB_PARAMETER_KEY = "CUSTOM_JOB_PARAMETER_ID"; + public static final String COB_PARAMETER = "loanCobParameter"; public static final Long NUMBER_OF_DAYS_BEHIND = 1L; + public static final String PARTITION_KEY = "partition"; + public static final String PARTITION_PREFIX = "partition_"; protected COBConstant() { diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java b/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java new file mode 100644 index 00000000000..5d27f3973d7 --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java @@ -0,0 +1,101 @@ +/** + * 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.common; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.COBConstant; +import org.apache.fineract.cob.data.BusinessStepNameAndOrder; +import org.apache.fineract.cob.data.COBParameter; +import org.apache.fineract.cob.data.COBPartition; +import org.apache.fineract.cob.resolver.BusinessDateResolver; +import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.launch.JobExecutionNotRunningException; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.launch.NoSuchJobExecutionException; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.util.StopWatch; + +@Slf4j +@RequiredArgsConstructor +public abstract class CommonPartitioner { + + private final JobOperator jobOperator; + private final StepExecution stepExecution; + private final Long numberOfDays; + private final RetrieveIdService retrieveIdService; + + public Map getPartitions(int partitionSize, Set cobBusinessSteps) { + if (cobBusinessSteps.isEmpty()) { + stopJobExecution(); + return Map.of(); + } + LocalDate businessDate = BusinessDateResolver.resolve(stepExecution); + boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution); + StopWatch sw = new StopWatch(); + sw.start(); + List partitions = new ArrayList<>( + retrieveIdService.retrieveLoanCOBPartitions(numberOfDays, businessDate, isCatchUp, partitionSize)); + sw.stop(); + // if there is no loan to be closed, we still would like to create at least one partition + + if (partitions.isEmpty()) { + partitions.add(new COBPartition(0L, 0L, 1L, 0L)); + } + log.info( + "{}} found {} loans to be processed as part of COB. {} partitions were created using partition size {}. RetrieveLoanCOBPartitions was executed in {} ms.", + getClass().getName(), getLoanCount(partitions), partitions.size(), partitionSize, sw.getTotalTimeMillis()); + return partitions.stream().collect(Collectors.toMap(l -> COBConstant.PARTITION_PREFIX + l.getPageNo(), + l -> createExecutionContextForPartition(cobBusinessSteps, l, businessDate, isCatchUp))); + } + + private long getLoanCount(List loanCOBPartitions) { + return loanCOBPartitions.stream().map(COBPartition::getCount).reduce(0L, Long::sum); + } + + private ExecutionContext createExecutionContextForPartition(Set cobBusinessSteps, + COBPartition loanCOBPartition, LocalDate businessDate, boolean isCatchUp) { + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put(COBConstant.BUSINESS_STEPS, cobBusinessSteps); + executionContext.put(COBConstant.COB_PARAMETER, new COBParameter(loanCOBPartition.getMinId(), loanCOBPartition.getMaxId())); + executionContext.put(COBConstant.PARTITION_KEY, COBConstant.PARTITION_PREFIX + loanCOBPartition.getPageNo()); + executionContext.put(COBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDate.toString()); + executionContext.put(COBConstant.IS_CATCH_UP_PARAMETER_NAME, Boolean.toString(isCatchUp)); + return executionContext; + } + + private void stopJobExecution() { + Long jobId = stepExecution.getJobExecution().getId(); + try { + jobOperator.stop(jobId); + } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) { + log.error("There is no running execution for the given execution ID. Execution ID: {}", jobId); + throw new RuntimeException(e); + } + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java b/fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java similarity index 96% rename from fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java index 70639d9fb4e..652d50bbee3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java @@ -29,6 +29,7 @@ public static COBParameter convert(Object obj) { if (obj instanceof COBParameter) { return (COBParameter) obj; } else if (obj instanceof LoanCOBParameter loanCOBParameter) { + // for backward compatibility return loanCOBParameter.toCOBParameter(); } return null; diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java b/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java index 5f084f458b0..ddad257e18b 100644 --- a/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java @@ -19,6 +19,10 @@ package org.apache.fineract.cob.data; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -31,4 +35,10 @@ public class BusinessStepNameAndOrder { private String stepName; private Long stepOrder; + + public static TreeMap getBusinessStepMap(Set businessSteps) { + Map businessStepMap = businessSteps.stream() + .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName)); + return new TreeMap<>(businessStepMap); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java b/fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java index a17c5a9bf75..6c9d7eb6d9a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java @@ -29,6 +29,7 @@ @NoArgsConstructor @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @EqualsAndHashCode +@Deprecated public class LoanCOBParameter { private Long minLoanId; diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java new file mode 100644 index 00000000000..3fdfab8c177 --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java @@ -0,0 +1,91 @@ +/** + * 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.domain; + +import java.sql.PreparedStatement; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.springframework.jdbc.core.JdbcTemplate; + +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractLockingService implements LockingService { + + private final JdbcTemplate jdbcTemplate; + private final FineractProperties fineractProperties; + private final AccountLockRepository loanAccountLockRepository; + + protected abstract String getBatchLoanLockUpgrade(); + + protected abstract String getBatchLoanLockInsert(); + + @Override + public void upgradeLock(List accountsToLock, LockOwner lockOwner) { + jdbcTemplate.batchUpdate(getBatchLoanLockUpgrade(), accountsToLock, getInClauseParameterSizeLimit(), (ps, id) -> { + ps.setString(1, lockOwner.name()); + ps.setObject(2, DateUtils.getAuditOffsetDateTime()); + ps.setLong(3, id); + }); + } + + @Override + public List findAllByLoanIdIn(List loanIds) { + return loanAccountLockRepository.findAllByLoanIdIn(loanIds); + } + + @Override + public T findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner) { + return loanAccountLockRepository.findByLoanIdAndLockOwner(loanId, lockOwner).orElseGet(() -> { + log.warn("There is no lock for loan account with id: {}", loanId); + return null; + }); + } + + @Override + public List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) { + return loanAccountLockRepository.findAllByLoanIdInAndLockOwner(loanIds, lockOwner); + } + + @Override + public void applyLock(List loanIds, LockOwner lockOwner) { + LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); + jdbcTemplate.batchUpdate(getBatchLoanLockInsert(), loanIds, loanIds.size(), (PreparedStatement ps, Long loanId) -> { + ps.setLong(1, loanId); + ps.setLong(2, 1); + ps.setString(3, lockOwner.name()); + ps.setObject(4, DateUtils.getAuditOffsetDateTime()); + ps.setObject(5, cobBusinessDate); + }); + } + + @Override + public void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) { + loanAccountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, lockOwner); + } + + private int getInClauseParameterSizeLimit() { + return fineractProperties.getQuery().getInClauseParameterSizeLimit(); + } +} diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java new file mode 100644 index 00000000000..5721b33e9a3 --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java @@ -0,0 +1,110 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; +import java.io.Serializable; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.springframework.data.domain.Persistable; + +@Getter +@MappedSuperclass +@NoArgsConstructor +public abstract class AccountLock implements Persistable, Serializable { + + protected static final long serialVersionUID = 2272591907035824317L; + + @Id + @Getter + @Column(name = "loan_id", nullable = false) + protected Long loanId; + + @Version + @Getter + @Column(name = "version") + protected Long version; + + @Enumerated(EnumType.STRING) + @Getter + @Column(name = "lock_owner", nullable = false) + protected LockOwner lockOwner; + + @Column(name = "lock_placed_on", nullable = false) + @Getter + protected OffsetDateTime lockPlacedOn; + + @Column(name = "error") + @Getter + protected String error; + + @Column(name = "stacktrace") + @Getter + protected String stacktrace; + + @Column(name = "lock_placed_on_cob_business_date") + @Getter + protected LocalDate lockPlacedOnCobBusinessDate; + + @Transient + @Setter(value = AccessLevel.NONE) + @Getter + protected boolean isNew = true; + + @PrePersist + @PostLoad + void markNotNew() { + this.isNew = false; + } + + @Override + public Long getId() { + return getLoanId(); + } + + public AccountLock(Long loanId, LockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) { + this.loanId = loanId; + this.lockOwner = lockOwner; + this.lockPlacedOn = DateUtils.getAuditOffsetDateTime(); + this.lockPlacedOnCobBusinessDate = lockPlacedOnCobBusinessDate; + } + + public void setError(String errorMessage, String stacktrace) { + this.error = errorMessage; + this.stacktrace = stacktrace; + } + + public void setNewLockOwner(LockOwner newLockOwner) { + this.lockOwner = newLockOwner; + this.lockPlacedOn = DateUtils.getAuditOffsetDateTime(); + } +} diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java new file mode 100644 index 00000000000..db5a031641e --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java @@ -0,0 +1,45 @@ +/** + * 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.domain; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface AccountLockRepository { + + Optional findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + + void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); + + List findAllByLoanIdIn(List loanIds); + + boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + + boolean existsByLoanIdAndLockOwnerAndErrorIsNotNull(Long loanId, LockOwner lockOwner); + + List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); + + void removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(List lockOwners); + + Page findAll(Pageable loanAccountLockPage); +} diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java index 765db8e28ca..ba5d8ed4f4a 100644 --- a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java @@ -18,7 +18,7 @@ */ package org.apache.fineract.cob.domain; -public interface CustomLoanAccountLockRepository { +public interface CustomLoanAccountLockRepository { void updateLoanFromAccountLocks(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockOwner.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockOwner.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java similarity index 71% rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java index 00830ba4e5f..7b2e12ec066 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java @@ -16,23 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.cob.loan; +package org.apache.fineract.cob.domain; import java.util.List; -import org.apache.fineract.cob.domain.LoanAccountLock; -import org.apache.fineract.cob.domain.LockOwner; -public interface LoanLockingService { +public interface LockingService { void upgradeLock(List accountsToLock, LockOwner lockOwner); void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); - List findAllByLoanIdIn(List loanIds); + List findAllByLoanIdIn(List loanIds); - LoanAccountLock findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + T findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); - List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); + List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); void applyLock(List loanIds, LockOwner lockOwner); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java similarity index 85% rename from fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java index 5d5c6e1c89c..aac761ce49e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java @@ -18,9 +18,9 @@ */ package org.apache.fineract.cob.exceptions; -public class LoanLockCannotBeAppliedException extends Exception { +public class LockCannotBeAppliedException extends Exception { - public LoanLockCannotBeAppliedException(String message, Throwable cause) { + public LockCannotBeAppliedException(String message, Throwable cause) { super(message, cause); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java similarity index 90% rename from fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java index 482f4f55b26..6a94b8c24a1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java @@ -18,11 +18,11 @@ */ package org.apache.fineract.cob.exceptions; -public class LoanReadException extends Exception { +public class LockedReadException extends Exception { private final Long id; - public LoanReadException(Long id, Throwable t) { + public LockedReadException(Long id, Throwable t) { super(String.format("Loan is in already locked state! loanId: %d", id), t); this.id = id; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java b/fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java similarity index 84% rename from fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java index e42c9973ca9..34e44f9292d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java @@ -23,13 +23,12 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.cob.domain.LoanAccountLock; +import org.apache.fineract.cob.domain.AccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.exceptions.LoanReadException; -import org.apache.fineract.cob.loan.LoanLockingService; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.exceptions.LockedReadException; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.serialization.ThrowableSerialization; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.batch.core.annotation.OnProcessError; import org.springframework.batch.core.annotation.OnReadError; import org.springframework.batch.core.annotation.OnSkipInProcess; @@ -44,9 +43,9 @@ @Slf4j @RequiredArgsConstructor -public abstract class AbstractLoanItemListener { +public abstract class AbstractLoanItemListener> { - private final LoanLockingService loanLockingService; + private final LockingService loanLockingService; private final TransactionTemplate transactionTemplate; @@ -57,7 +56,7 @@ private void updateAccountLockWithError(List loanIds, String msg, Throwabl @Override protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) { for (Long loanId : loanIds) { - LoanAccountLock loanAccountLock = loanLockingService.findByLoanIdAndLockOwner(loanId, getLockOwner()); + T loanAccountLock = loanLockingService.findByLoanIdAndLockOwner(loanId, getLockOwner()); if (loanAccountLock != null) { loanAccountLock.setError(String.format(msg, loanId), ThrowableSerialization.serialize(e)); } @@ -68,7 +67,7 @@ protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) { @OnReadError public void onReadError(Exception e) { - if (e instanceof LoanReadException ee) { + if (e instanceof LockedReadException ee) { log.warn("Error was triggered during reading of Loan (id={}) due to: {}", ee.getId(), ThrowableSerialization.serialize(e)); updateAccountLockWithError(List.of(ee.getId()), "Loan (id: %d) reading is failed", e); } else { @@ -77,13 +76,13 @@ public void onReadError(Exception e) { } @OnProcessError - public void onProcessError(@NonNull Loan item, Exception e) { + public void onProcessError(@NonNull S item, Exception e) { log.warn("Error was triggered during processing of Loan (id={}) due to: {}", item.getId(), ThrowableSerialization.serialize(e)); updateAccountLockWithError(List.of(item.getId()), "Loan (id: %d) processing is failed", e); } @OnWriteError - public void onWriteError(Exception e, @NonNull Chunk items) { + public void onWriteError(Exception e, @NonNull Chunk items) { List loanIds = items.getItems().stream().map(AbstractPersistableCustom::getId).toList(); log.warn("Error was triggered during writing of Loans (ids={}) due to: {}", loanIds, ThrowableSerialization.serialize(e)); @@ -96,12 +95,12 @@ public void onSkipInRead(@NonNull Throwable e) { } @OnSkipInProcess - public void onSkipInProcess(@NonNull Loan item, @NonNull Throwable e) { + public void onSkipInProcess(@NonNull S item, @NonNull Throwable e) { log.warn("Skipping was triggered during processing of Loan (id={})", item.getId()); } @OnSkipInWrite - public void onSkipInWrite(@NonNull Loan item, @NonNull Throwable e) { + public void onSkipInWrite(@NonNull S item, @NonNull Throwable e) { log.warn("Skipping was triggered during writing of Loan (id={})", item.getId()); } diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java b/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java new file mode 100644 index 00000000000..9efdeb1d7e9 --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java @@ -0,0 +1,75 @@ +/** + * 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.processor; + +import static org.apache.fineract.cob.data.BusinessStepNameAndOrder.getBusinessStepMap; + +import java.time.LocalDate; +import java.util.Set; +import java.util.TreeMap; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.data.BusinessStepNameAndOrder; +import org.apache.fineract.cob.resolver.BusinessDateResolver; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.annotation.AfterStep; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.lang.NonNull; + +@RequiredArgsConstructor +public abstract class AbstractItemProcessor> implements ItemProcessor { + + private final COBBusinessStepService cobBusinessStepService; + + @Setter + private ExecutionContext executionContext; + + @Getter + private LocalDate businessDate; + + @SuppressWarnings({ "unchecked" }) + @Override + public I process(@NonNull I item) throws Exception { + Set businessSteps = (Set) executionContext.get("businessSteps"); + if (businessSteps == null) { + throw new IllegalStateException("No business steps found in the execution context"); + } + TreeMap businessStepMap = getBusinessStepMap(businessSteps); + + I alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, item); + setLastRun(alreadyProcessedLoan); + return alreadyProcessedLoan; + } + + protected void setBusinessDate(StepExecution stepExecution) { + businessDate = BusinessDateResolver.resolve(stepExecution); + } + + @AfterStep + public ExitStatus afterStep(@NonNull StepExecution stepExecution) { + return ExitStatus.COMPLETED; + } + + public abstract void setLastRun(I processedLoan); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java similarity index 93% rename from fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java index e91dabbbc80..7b57f89407e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java @@ -19,7 +19,7 @@ package org.apache.fineract.cob.resolver; import java.time.LocalDate; -import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.cob.COBConstant; import org.springframework.batch.core.StepExecution; public final class BusinessDateResolver { @@ -27,7 +27,7 @@ public final class BusinessDateResolver { private BusinessDateResolver() {} public static LocalDate resolve(StepExecution stepExecution) { - Object bd = stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME); + Object bd = stepExecution.getJobExecution().getExecutionContext().get(COBConstant.BUSINESS_DATE_PARAMETER_NAME); return switch (bd) { case null -> throw new IllegalStateException( "Missing BusinessDate in JobExecutionContext for jobExecutionId=" + stepExecution.getJobExecution().getId()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java similarity index 92% rename from fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java index 25afa79c45e..6ddf59df585 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java @@ -18,7 +18,7 @@ */ package org.apache.fineract.cob.resolver; -import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.cob.COBConstant; import org.springframework.batch.core.StepExecution; public final class CatchUpFlagResolver { @@ -26,7 +26,7 @@ public final class CatchUpFlagResolver { private CatchUpFlagResolver() {} public static boolean resolve(StepExecution stepExecution) { - Object isCatchUp = stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME); + Object isCatchUp = stepExecution.getJobExecution().getExecutionContext().get(COBConstant.IS_CATCH_UP_PARAMETER_NAME); return switch (isCatchUp) { case null -> false; case String isCatchUpStr -> Boolean.parseBoolean(isCatchUpStr); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java similarity index 71% rename from fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java index 1077c7afad0..29d5aa8a531 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java @@ -20,26 +20,26 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.apache.fineract.cob.domain.LoanAccountLock; -import org.apache.fineract.cob.domain.LoanAccountLockRepository; +import org.apache.fineract.cob.domain.AccountLock; +import org.apache.fineract.cob.domain.AccountLockRepository; +import org.apache.fineract.cob.domain.CustomLoanAccountLockRepository; import org.apache.fineract.cob.domain.LockOwner; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -@Service @RequiredArgsConstructor -public class LoanAccountLockServiceImpl implements LoanAccountLockService { +public abstract class AbstractAccountLockService implements AccountLockService { - private final LoanAccountLockRepository loanAccountLockRepository; + private final AccountLockRepository loanAccountLockRepository; + private final CustomLoanAccountLockRepository customLoanAccountLockRepository; @Override - public List getLockedLoanAccountByPage(int page, int limit) { + public List getLockedLoanAccountByPage(int page, int limit) { Pageable loanAccountLockPage = PageRequest.of(page, limit); - Page loanAccountLocks = loanAccountLockRepository.findAll(loanAccountLockPage); + Page loanAccountLocks = loanAccountLockRepository.findAll(loanAccountLockPage); return loanAccountLocks.getContent(); } @@ -58,8 +58,9 @@ public boolean isLockOverrulable(Long loanId) { @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateCobAndRemoveLocks() { - loanAccountLockRepository.updateLoanFromAccountLocks(); - loanAccountLockRepository.removeLockByOwner(); + customLoanAccountLockRepository.updateLoanFromAccountLocks(); + loanAccountLockRepository.removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull( + List.of(LockOwner.LOAN_COB_CHUNK_PROCESSING, LockOwner.LOAN_INLINE_COB_PROCESSING)); } } diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java new file mode 100644 index 00000000000..4f55670ac2f --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java @@ -0,0 +1,33 @@ +/** + * 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.service; + +import java.util.List; +import org.apache.fineract.cob.domain.AccountLock; + +public interface AccountLockService { + + List getLockedLoanAccountByPage(int page, int limit); + + boolean isLoanHardLocked(Long loanId); + + boolean isLockOverrulable(Long loanId); + + void updateCobAndRemoveLocks(); +} diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java new file mode 100644 index 00000000000..c79d761829c --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java @@ -0,0 +1,69 @@ +/** + * 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.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.LinkedBlockingQueue; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.COBConstant; +import org.apache.fineract.cob.converter.COBParameterConverter; +import org.apache.fineract.cob.data.COBParameter; +import org.apache.fineract.cob.domain.AccountLock; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.lang.NonNull; + +@RequiredArgsConstructor +public class BeforeStepLockingItemReaderHelper { + + private final RetrieveIdService retrieveIdService; + private final LockingService loanLockingService; + + @SuppressWarnings({ "unchecked" }) + public LinkedBlockingQueue filterRemainingData(@NonNull StepExecution stepExecution) { + ExecutionContext executionContext = stepExecution.getExecutionContext(); + COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(COBConstant.COB_PARAMETER)); + List loanIds; + boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution); + if (Objects.isNull(loanCOBParameter) + || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId())) + || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) { + loanIds = Collections.emptyList(); + } else { + loanIds = retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp); + if (!loanIds.isEmpty()) { + List lockedByCOBChunkProcessingAccountIds = getLoanIdsLockedWithChunkProcessingLock(loanIds); + loanIds.retainAll(lockedByCOBChunkProcessingAccountIds); + } + } + return new LinkedBlockingQueue<>(loanIds); + } + + private List getLoanIdsLockedWithChunkProcessingLock(List loanIds) { + List accountLocks = new ArrayList<>( + loanLockingService.findAllByLoanIdInAndLockOwner(loanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING)); + return accountLocks.stream().map(T::getLoanId).toList(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java similarity index 85% rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java rename to fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java index 590757fc741..a934a161c3e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java @@ -16,8 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.cob.loan; +package org.apache.fineract.cob.service; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDate; import java.util.List; import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo; @@ -26,7 +28,7 @@ import org.apache.fineract.cob.data.COBPartition; import org.springframework.data.repository.query.Param; -public interface RetrieveLoanIdService { +public interface RetrieveIdService { List retrieveLoanCOBPartitions(Long numberOfDays, LocalDate businessDate, boolean isCatchUp, int partitionSize); @@ -41,4 +43,8 @@ public interface RetrieveLoanIdService { List findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate); List retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List loanIds); + + static COBPartition mapRow(ResultSet rs, int rowNum) throws SQLException { + return new COBPartition(rs.getLong("min"), rs.getLong("max"), rs.getLong("page"), rs.getLong("count")); + } } diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java b/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java new file mode 100644 index 00000000000..b1fcd63d446 --- /dev/null +++ b/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java @@ -0,0 +1,119 @@ +/** + * 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.tasklet; + +import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW; + +import com.google.common.collect.Lists; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.converter.COBParameterConverter; +import org.apache.fineract.cob.data.COBParameter; +import org.apache.fineract.cob.domain.AccountLock; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.exceptions.LockCannotBeAppliedException; +import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +@RequiredArgsConstructor +public abstract class ApplyCommonLockTasklet implements Tasklet { + + private static final long NUMBER_OF_RETRIES = 3; + private final FineractProperties fineractProperties; + private final LockingService loanLockingService; + private final RetrieveIdService retrieveIdService; + private final TransactionTemplate transactionTemplate; + + public abstract String getCOBParameter(); + + public abstract LockOwner getLockOwner(); + + @Override + @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") + public RepeatStatus execute(@NonNull StepContribution contribution, @NonNull ChunkContext chunkContext) + throws LockCannotBeAppliedException { + ExecutionContext executionContext = contribution.getStepExecution().getExecutionContext(); + long numberOfExecutions = contribution.getStepExecution().getCommitCount(); + COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(getCOBParameter())); + boolean isCatchUp = CatchUpFlagResolver.resolve(contribution.getStepExecution()); + List loanIds; + if (Objects.isNull(loanCOBParameter) + || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId())) + || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) { + loanIds = Collections.emptyList(); + } else { + loanIds = new ArrayList<>( + retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp)); + } + List> loanIdPartitions = Lists.partition(loanIds, getInClauseParameterSizeLimit()); + List accountLocks = new ArrayList<>(); + loanIdPartitions.forEach(loanIdPartition -> accountLocks.addAll(loanLockingService.findAllByLoanIdIn(loanIdPartition))); + + List toBeProcessedLoanIds = new ArrayList<>(loanIds); + List alreadyLockedAccountIds = accountLocks.stream().map(AccountLock::getLoanId).toList(); + + toBeProcessedLoanIds.removeAll(alreadyLockedAccountIds); + try { + applyLocks(toBeProcessedLoanIds); + } catch (Exception e) { + if (numberOfExecutions > NUMBER_OF_RETRIES) { + String message = "There was an error applying lock to loan accounts."; + log.error("{}", message, e); + throw new LockCannotBeAppliedException(message, e); + } else { + return RepeatStatus.CONTINUABLE; + } + } + + return RepeatStatus.FINISHED; + } + + private void applyLocks(List toBeProcessedLoanIds) { + transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW); + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) { + log.info("Apply locks for {} by owner {}", toBeProcessedLoanIds, getLockOwner()); + loanLockingService.applyLock(toBeProcessedLoanIds, getLockOwner()); + } + }); + } + + private int getInClauseParameterSizeLimit() { + return fineractProperties.getQuery().getInClauseParameterSizeLimit(); + } +} diff --git a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml index 509a60dd916..af0d95e2a35 100644 --- a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml +++ b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml @@ -65,7 +65,7 @@ org.apache.fineract.infrastructure.businessdate.domain.BusinessDate org.apache.fineract.infrastructure.codes.domain.CodeValue - + false diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 16f9ad9c29e..51a501599e9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -60,6 +60,7 @@ public enum JobName { ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), // ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), // JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), // + WORKING_CAPITAL_LOAN_COB_JOB("Working Capital Loan COB"), // ; // private final String name; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java diff --git a/fineract-e2e-tests-core/build.gradle b/fineract-e2e-tests-core/build.gradle index d4cb2af8f43..53c3ff7e6ff 100644 --- a/fineract-e2e-tests-core/build.gradle +++ b/fineract-e2e-tests-core/build.gradle @@ -99,6 +99,10 @@ dependencies { testImplementation 'io.github.classgraph:classgraph:4.8.179' testImplementation 'org.apache.commons:commons-collections4:4.4' + + testImplementation 'org.springframework:spring-jdbc' + testImplementation 'org.postgresql:postgresql' + testImplementation 'org.mariadb.jdbc:mariadb-java-client' } tasks.withType(JavaCompile).configureEach { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java new file mode 100644 index 00000000000..909501b7f76 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java @@ -0,0 +1,68 @@ +/** + * 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.test.config; + +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +@Configuration +@Slf4j +public class TestDatabaseConfiguration { + + @Value("${fineract-test.db.protocol}") + private String protocol; + + @Value("${fineract-test.db.hostname}") + private String hostname; + + @Value("${fineract-test.db.port}") + private String port; + + @Value("${fineract-test.db.name}") + private String dbName; + + @Value("${fineract-test.db.username}") + private String username; + + @Value("${fineract-test.db.password}") + private String password; + + @Bean + public DataSource testDataSource() { + // DriverManagerDataSource creates a new connection per call (no pooling). + // This is intentional for lightweight e2e test usage — no pool management overhead needed. + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + String url = protocol + "://" + hostname + ":" + port + "/" + dbName; + log.debug("Test database URL: {}", url); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + return dataSource; + } + + @Bean + public JdbcTemplate testJdbcTemplate(DataSource testDataSource) { + return new JdbcTemplate(testDataSource); + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java index 5a02805d7f0..c3d2e8c9308 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java @@ -27,6 +27,8 @@ public enum LoanStatus { SUBMITTED_AND_PENDING_APPROVAL(100), // APPROVED(200), // ACTIVE(300), // + TRANSFER_IN_PROGRESS(303), // + TRANSFER_ON_HOLD(304), // WITHDRAWN(400), // REJECTED(500), // CLOSED_OBLIGATIONS_MET(600), // diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java index f9f97f312c4..347f3fca3a0 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java @@ -28,7 +28,8 @@ public enum DefaultJob implements Job { ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting", "ACC_ACPO"), // ADD_ACCRUAL_TRANSACTIONS_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS( "Add Accrual Transactions For Loans With Income Posted As Transactions", "LA_AATR"), // - RECALCULATE_INTEREST_FOR_LOANS("Recalculate Interest For Loans", "LA_RINT"); // + RECALCULATE_INTEREST_FOR_LOANS("Recalculate Interest For Loans", "LA_RINT"), // + WORKING_CAPITAL_LOAN_COB("Working Capital Loan COB", "WC_COB"); // private final String customName; private final String shortName; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java index c1c5621a864..94c27c39fc2 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java @@ -30,7 +30,9 @@ import org.apache.fineract.client.feign.FineractFeignClient; import org.apache.fineract.client.models.BusinessStep; import org.apache.fineract.client.models.BusinessStepRequest; +import org.apache.fineract.client.models.ConfiguredJobNamesDTO; import org.apache.fineract.client.models.JobBusinessStepConfigData; +import org.apache.fineract.client.models.JobBusinessStepDetail; import org.springframework.stereotype.Component; @RequiredArgsConstructor @@ -62,6 +64,46 @@ public void setWorkflowJobs() { logChanges(); } + /** + * Returns all job names that have business step configuration registered. + */ + public ConfiguredJobNamesDTO getConfiguredBusinessJobs() { + return ok(() -> fineractClient.businessStepConfiguration().retrieveAllConfiguredBusinessJobs(Map.of())); + } + + /** + * Returns the currently configured business steps for the given job. + * + * @param jobName + * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS} + */ + public JobBusinessStepConfigData getConfiguredWorkflowSteps(String jobName) { + return ok(() -> fineractClient.businessStepConfiguration().retrieveAllConfiguredBusinessStep(jobName, Map.of())); + } + + /** + * Returns all available (registered) business steps for the given job. + * + * @param jobName + * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS} + */ + public JobBusinessStepDetail getAvailableWorkflowSteps(String jobName) { + return ok(() -> fineractClient.businessStepConfiguration().retrieveAllAvailableBusinessStep(jobName, Map.of())); + } + + /** + * Replaces the configured business steps for the given job. + * + * @param jobName + * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS} + * @param steps + * the ordered list of business steps to configure + */ + public void updateWorkflowSteps(String jobName, List steps) { + BusinessStepRequest request = new BusinessStepRequest().businessSteps(steps); + executeVoid(() -> fineractClient.businessStepConfiguration().updateJobBusinessStepConfig(jobName, request, Map.of())); + } + private void logChanges() { JobBusinessStepConfigData changesResponse = ok(() -> fineractClient.businessStepConfiguration() .retrieveAllConfiguredBusinessStep(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, Map.of())); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java new file mode 100644 index 00000000000..55715e29dac --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java @@ -0,0 +1,88 @@ +/** + * 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.test.helper; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.test.data.LoanStatus; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class WorkingCapitalLoanTestHelper { + + private static final String TABLE_WC_LOAN = "m_wc_loan"; + private static final String TABLE_WC_LOAN_ACCOUNT_LOCKS = "m_wc_loan_account_locks"; + private static final int INITIAL_VERSION = 0; + private static final long ADMIN_USER_ID = 1L; + + private final JdbcTemplate testJdbcTemplate; + private final SimpleJdbcInsert wcLoanInsert; + + public WorkingCapitalLoanTestHelper(JdbcTemplate testJdbcTemplate) { + this.testJdbcTemplate = testJdbcTemplate; + this.wcLoanInsert = new SimpleJdbcInsert(testJdbcTemplate)// + .withTableName(TABLE_WC_LOAN)// + .usingGeneratedKeyColumns("id"); + } + + public Long insertActiveLoan() { + return insertLoan(LoanStatus.ACTIVE.getValue(), null); + } + + public Long insertLoan(int loanStatusId, LocalDate lastClosedBusinessDate) { + Timestamp now = Timestamp.from(OffsetDateTime.now(ZoneOffset.UTC).toInstant()); + MapSqlParameterSource params = new MapSqlParameterSource()// + .addValue("version", INITIAL_VERSION)// + .addValue("created_by", ADMIN_USER_ID)// + .addValue("last_modified_by", ADMIN_USER_ID)// + .addValue("created_on_utc", now)// + .addValue("last_modified_on_utc", now)// + .addValue("loan_status_id", loanStatusId)// + .addValue("last_closed_business_date", lastClosedBusinessDate); + Number key = wcLoanInsert.executeAndReturnKey(params); + return Objects.requireNonNull(key, "Generated key must not be null").longValue(); + } + + public LocalDate getLastClosedBusinessDate(Long loanId) { + return testJdbcTemplate.queryForObject("SELECT last_closed_business_date FROM " + TABLE_WC_LOAN + " WHERE id = ?", LocalDate.class, + loanId); + } + + public int getVersion(Long loanId) { + return testJdbcTemplate.queryForObject("SELECT version FROM " + TABLE_WC_LOAN + " WHERE id = ?", Integer.class, loanId); + } + + public int countLocksByLoanId(Long loanId) { + return testJdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + TABLE_WC_LOAN_ACCOUNT_LOCKS + " WHERE loan_id = ?", Integer.class, + loanId); + } + + public void deleteById(Long loanId) { + testJdbcTemplate.update("DELETE FROM " + TABLE_WC_LOAN_ACCOUNT_LOCKS + " WHERE loan_id = ?", loanId); + testJdbcTemplate.update("DELETE FROM " + TABLE_WC_LOAN + " WHERE id = ?", loanId); + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java index 269d3c9de07..e97fd26afc5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java @@ -20,6 +20,7 @@ import java.util.List; import org.apache.fineract.test.config.CacheConfiguration; +import org.apache.fineract.test.config.TestDatabaseConfiguration; import org.apache.fineract.test.initializer.global.FineractGlobalInitializerStep; import org.apache.fineract.test.initializer.scenario.FineractScenarioInitializerStep; import org.apache.fineract.test.initializer.suite.FineractSuiteInitializerStep; @@ -32,7 +33,7 @@ @Configuration @ComponentScan({ "org.apache.fineract.test.api", "org.apache.fineract.test.helper" }) @PropertySource("classpath:fineract-test-application.properties") -@Import({ CacheConfiguration.class }) +@Import({ CacheConfiguration.class, TestDatabaseConfiguration.class }) public class BaseFineractInitializerConfiguration { @Bean diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java new file mode 100644 index 00000000000..365a55409b1 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java @@ -0,0 +1,131 @@ +/** + * 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.test.stepdef.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.BusinessStep; +import org.apache.fineract.client.models.BusinessStepDetail; +import org.apache.fineract.client.models.ConfiguredJobNamesDTO; +import org.apache.fineract.client.models.JobBusinessStepConfigData; +import org.apache.fineract.client.models.JobBusinessStepDetail; +import org.apache.fineract.test.helper.WorkFlowJobHelper; +import org.apache.fineract.test.stepdef.AbstractStepDef; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class BusinessStepConfigurationStepDef extends AbstractStepDef { + + @Autowired + private WorkFlowJobHelper workFlowJobHelper; + + @Then("Admin checks that configured business jobs contain {string}") + public void checkConfiguredBusinessJobsContain(String jobName) { + ConfiguredJobNamesDTO response = workFlowJobHelper.getConfiguredBusinessJobs(); + List businessJobs = response.getBusinessJobs(); + log.debug("Configured business jobs: {}", businessJobs); + assertThat(businessJobs)// + .as("Configured business jobs should contain '%s' but got: %s", jobName, businessJobs)// + .contains(jobName); + } + + @Then("Admin verifies configured business steps for {string} match:") + public void verifyConfiguredBusinessStepsMatch(String jobName, DataTable dataTable) { + JobBusinessStepConfigData response = workFlowJobHelper.getConfiguredWorkflowSteps(jobName); + List actualSteps = response.getBusinessSteps(); + log.debug("Configured business steps for '{}': {}", jobName, actualSteps); + + List> expectedRows = dataTable.asMaps(String.class, String.class); + assertThat(actualSteps)// + .as("Configured steps count for '%s' — expected %d but got %d: %s", jobName, expectedRows.size(), actualSteps.size(), + actualSteps)// + .hasSize(expectedRows.size()); + + List sortedActual = actualSteps.stream()// + .sorted(Comparator.comparingLong(BusinessStep::getOrder))// + .toList(); + + for (int i = 0; i < expectedRows.size(); i++) { + Map expected = expectedRows.get(i); + BusinessStep actual = sortedActual.get(i); + String expectedStepName = expected.get("stepName"); + long expectedOrder = Long.parseLong(expected.get("order")); + assertThat(actual.getStepName())// + .as("Step at position %d for job '%s' — expected name '%s' but got '%s'", i, jobName, expectedStepName, + actual.getStepName())// + .isEqualTo(expectedStepName); + assertThat(actual.getOrder())// + .as("Step '%s' for job '%s' — expected order %d but got %d", expectedStepName, jobName, expectedOrder, + actual.getOrder())// + .isEqualTo(expectedOrder); + } + } + + @Then("Admin verifies available business steps for {string} contain:") + public void verifyAvailableBusinessStepsContain(String jobName, DataTable dataTable) { + JobBusinessStepDetail response = workFlowJobHelper.getAvailableWorkflowSteps(jobName); + List actualStepNames = response.getAvailableBusinessSteps().stream()// + .map(BusinessStepDetail::getStepName)// + .toList(); + log.debug("Available business steps for '{}': {}", jobName, actualStepNames); + + List> expectedRows = dataTable.asMaps(String.class, String.class); + for (Map row : expectedRows) { + String expectedStepName = row.get("stepName"); + assertThat(actualStepNames)// + .as("Available steps for '%s' should contain '%s' but got: %s", jobName, expectedStepName, actualStepNames)// + .contains(expectedStepName); + } + } + + @When("Admin updates business steps for {string} with:") + public void updateBusinessSteps(String jobName, DataTable dataTable) { + List> rows = dataTable.asMaps(String.class, String.class); + List steps = rows.stream()// + .map(row -> new BusinessStep()// + .stepName(row.get("stepName"))// + .order(Long.parseLong(row.get("order"))))// + .toList(); + log.debug("Updating business steps for '{}': {}", jobName, steps); + workFlowJobHelper.updateWorkflowSteps(jobName, steps); + } + + @Then("Admin fails to update business steps for {string} with invalid step {string}") + public void updateBusinessStepsFailsWithInvalidStep(String jobName, String invalidStepName) { + List steps = List.of(new BusinessStep().stepName(invalidStepName).order(1L)); + try { + workFlowJobHelper.updateWorkflowSteps(jobName, steps); + throw new AssertionError( + "Expected update to fail for invalid step '%s' on job '%s' but it succeeded".formatted(invalidStepName, jobName)); + } catch (CallFailedRuntimeException e) { + log.debug("Business step update correctly rejected for '{}' on job '{}': status={}", invalidStepName, jobName, e.getStatus()); + assertThat(e.getStatus())// + .as("Expected HTTP 400 for invalid step '%s' on job '%s' but got %d", invalidStepName, jobName, e.getStatus())// + .isEqualTo(400); + } + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/SchedulerStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/SchedulerStepDef.java index f8baa6b6001..5b19f9ac15c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/SchedulerStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/SchedulerStepDef.java @@ -18,9 +18,16 @@ */ package org.apache.fineract.test.stepdef.common; +import static org.apache.fineract.client.feign.util.FeignCalls.ok; +import static org.assertj.core.api.Assertions.assertThat; + import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.models.GetJobsResponse; import org.apache.fineract.test.data.job.DefaultJob; import org.apache.fineract.test.service.JobService; import org.apache.fineract.test.stepdef.AbstractStepDef; @@ -32,6 +39,9 @@ public class SchedulerStepDef extends AbstractStepDef { @Autowired private JobService jobService; + @Autowired + private FineractFeignClient fineractClient; + @And("Admin runs the Add Accrual Transactions job") public void runAccrualTransaction() { jobService.executeAndWait(DefaultJob.ADD_ACCRUAL_TRANSACTIONS); @@ -71,4 +81,29 @@ public void runCOB() { public void runAccrualActivityPosting() { jobService.executeAndWait(DefaultJob.ACCRUAL_ACTIVITY_POSTING); } + + @When("Admin runs WC COB job") + public void runWorkingCapitalLoanCOB() { + jobService.executeAndWait(DefaultJob.WORKING_CAPITAL_LOAN_COB); + } + + @Then("Admin verifies scheduler job {string} has display name {string}") + public void verifyJobDisplayName(String shortName, String expectedDisplayName) { + GetJobsResponse response = ok(() -> fineractClient.schedulerJob().retrieveByShortName(shortName, Map.of())); + assertThat(response.getDisplayName())// + .as("Job '%s' display name — expected '%s' but got '%s'", shortName, expectedDisplayName, response.getDisplayName())// + .isEqualTo(expectedDisplayName); + } + + @Then("Admin verifies scheduler job {string} has active status {string}") + public void verifyJobActiveStatus(String shortName, String expectedActive) { + assertThat(expectedActive)// + .as("Parameter must be 'true' or 'false' but got '%s'", expectedActive)// + .isIn("true", "false"); + GetJobsResponse response = ok(() -> fineractClient.schedulerJob().retrieveByShortName(shortName, Map.of())); + boolean expected = Boolean.parseBoolean(expectedActive); + assertThat(response.getActive())// + .as("Job '%s' active status — expected %s but got %s", shortName, expected, response.getActive())// + .isEqualTo(expected); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/WorkingCapitalLoanCobStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/WorkingCapitalLoanCobStepDef.java new file mode 100644 index 00000000000..a73e3ccd21d --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/WorkingCapitalLoanCobStepDef.java @@ -0,0 +1,185 @@ +/** + * 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.test.stepdef.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.test.data.LoanStatus; +import org.apache.fineract.test.helper.WorkingCapitalLoanTestHelper; +import org.apache.fineract.test.stepdef.AbstractStepDef; +import org.apache.fineract.test.support.TestContextKey; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class WorkingCapitalLoanCobStepDef extends AbstractStepDef { + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd MMMM yyyy"); + + @Autowired + private WorkingCapitalLoanTestHelper wcLoanHelper; + + @Before(value = "@WCCOBFeature") + public void beforeWcCobScenario() { + testContext().set(TestContextKey.WC_LOAN_IDS, new ArrayList()); + } + + // order > 10000 (default) so this cleanup runs before other @After hooks that may depend on DB state + @After(value = "@WCCOBFeature", order = 10001) + public void afterWcCobScenario() { + List loanIds = getTrackedLoanIds(); + if (!loanIds.isEmpty()) { + log.debug("After hook: cleaning up {} WC loan(s)", loanIds.size()); + for (Long loanId : loanIds) { + try { + wcLoanHelper.deleteById(loanId); + log.debug("After hook: deleted WC loan id={}", loanId); + } catch (Exception e) { + log.warn("After hook: failed to delete WC loan id={}: {}", loanId, e.getMessage()); + } + } + loanIds.clear(); + } + } + + @Given("Admin inserts an active WC loan into the database") + public void insertActiveWcLoan() { + Long loanId = wcLoanHelper.insertActiveLoan(); + log.debug("Inserted WC loan with id={}", loanId); + getTrackedLoanIds().add(loanId); + } + + @Given("Admin inserts {int} active WC loans into the database") + public void insertMultipleActiveWcLoans(int count) { + for (int i = 0; i < count; i++) { + Long loanId = wcLoanHelper.insertActiveLoan(); + log.debug("Inserted WC loan with id={}", loanId); + getTrackedLoanIds().add(loanId); + } + } + + @Then("Admin verifies all inserted WC loans have lastClosedBusinessDate {string}") + public void verifyAllLoansHaveLastClosedBusinessDate(String expectedDate) { + LocalDate expected = LocalDate.parse(expectedDate, DATE_FORMAT); + List loanIds = getTrackedLoanIds(); + assertThat(loanIds).as("No WC loan IDs tracked in test context").isNotEmpty(); + for (Long loanId : loanIds) { + LocalDate actual = wcLoanHelper.getLastClosedBusinessDate(loanId); + log.debug("WC loan id={} lastClosedBusinessDate={}", loanId, actual); + assertThat(actual)// + .as("WC loan id=%d — expected lastClosedBusinessDate '%s' but got '%s'", loanId, expected, actual)// + .isEqualTo(expected); + } + } + + @Given("Admin inserts a WC loan with status {string} into the database") + public void insertWcLoanWithStatus(String statusName) { + LoanStatus status = LoanStatus.valueOf(statusName); + Long loanId = wcLoanHelper.insertLoan(status.getValue(), null); + log.debug("Inserted WC loan with id={}, status={}({})", loanId, statusName, status.getValue()); + getTrackedLoanIds().add(loanId); + } + + @Given("Admin inserts a WC loan with status {string} and lastClosedBusinessDate {string} into the database") + public void insertWcLoanWithStatusAndDate(String statusName, String dateStr) { + LoanStatus status = LoanStatus.valueOf(statusName); + LocalDate lastClosedBusinessDate = LocalDate.parse(dateStr, DATE_FORMAT); + Long loanId = wcLoanHelper.insertLoan(status.getValue(), lastClosedBusinessDate); + log.debug("Inserted WC loan with id={}, status={}({}), lastClosedBusinessDate={}", loanId, statusName, status.getValue(), + lastClosedBusinessDate); + getTrackedLoanIds().add(loanId); + } + + @Then("Admin verifies all inserted WC loans have null lastClosedBusinessDate") + public void verifyAllLoansHaveNullLastClosedBusinessDate() { + List loanIds = getTrackedLoanIds(); + assertThat(loanIds).as("No WC loan IDs tracked in test context").isNotEmpty(); + for (Long loanId : loanIds) { + LocalDate actual = wcLoanHelper.getLastClosedBusinessDate(loanId); + log.debug("WC loan id={} lastClosedBusinessDate={}", loanId, actual); + assertThat(actual)// + .as("WC loan id=%d — expected null lastClosedBusinessDate but got '%s'", loanId, actual)// + .isNull(); + } + } + + @Then("Admin verifies all inserted WC loans have version {int}") + public void verifyAllLoansHaveVersion(int expectedVersion) { + List loanIds = getTrackedLoanIds(); + assertThat(loanIds).as("No WC loan IDs tracked in test context").isNotEmpty(); + for (Long loanId : loanIds) { + int actual = wcLoanHelper.getVersion(loanId); + log.debug("WC loan id={} version={}", loanId, actual); + assertThat(actual)// + .as("WC loan id=%d — expected version %d but got %d", loanId, expectedVersion, actual)// + .isEqualTo(expectedVersion); + } + } + + @Then("Admin verifies all inserted WC loans have no account locks") + public void verifyAllLoansHaveNoAccountLocks() { + List loanIds = getTrackedLoanIds(); + assertThat(loanIds).as("No WC loan IDs tracked in test context").isNotEmpty(); + for (Long loanId : loanIds) { + int lockCount = wcLoanHelper.countLocksByLoanId(loanId); + log.debug("WC loan id={} lock count={}", loanId, lockCount); + assertThat(lockCount)// + .as("WC loan id=%d — expected 0 account locks but got %d", loanId, lockCount)// + .isZero(); + } + } + + @Then("Admin verifies inserted WC loan {int} has lastClosedBusinessDate {string}") + public void verifyLoanAtIndexHasLastClosedBusinessDate(int index, String expectedDate) { + LocalDate expected = LocalDate.parse(expectedDate, DATE_FORMAT); + List loanIds = getTrackedLoanIds(); + assertThat(index).as("Loan index %d out of range (1..%d)", index, loanIds.size()).isBetween(1, loanIds.size()); + Long loanId = loanIds.get(index - 1); + LocalDate actual = wcLoanHelper.getLastClosedBusinessDate(loanId); + log.debug("WC loan index={} id={} lastClosedBusinessDate={}", index, loanId, actual); + assertThat(actual)// + .as("WC loan index=%d id=%d — expected lastClosedBusinessDate '%s' but got '%s'", index, loanId, expected, actual)// + .isEqualTo(expected); + } + + @Then("Admin verifies inserted WC loan {int} has null lastClosedBusinessDate") + public void verifyLoanAtIndexHasNullLastClosedBusinessDate(int index) { + List loanIds = getTrackedLoanIds(); + assertThat(index).as("Loan index %d out of range (1..%d)", index, loanIds.size()).isBetween(1, loanIds.size()); + Long loanId = loanIds.get(index - 1); + LocalDate actual = wcLoanHelper.getLastClosedBusinessDate(loanId); + log.debug("WC loan index={} id={} lastClosedBusinessDate={}", index, loanId, actual); + assertThat(actual)// + .as("WC loan index=%d id=%d — expected null lastClosedBusinessDate but got '%s'", index, loanId, actual)// + .isNull(); + } + + @SuppressWarnings("unchecked") + private List getTrackedLoanIds() { + return testContext().get(TestContextKey.WC_LOAN_IDS); + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 4c57191c92f..36a10e21740 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -317,4 +317,5 @@ public abstract class TestContextKey { public static final String OFFICE_CREATE_RESPONSE = "officeCreateResponse"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL_INTEREST_RECALC = "loanProductCreateResponseLP2DownPaymentAdvancedPaymentAllocationProgressiveLoanScheduleVerticalInterestRecalc"; public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP = "workingCapitalLoanProductCreateResponseWCLP"; + public static final String WC_LOAN_IDS = "wcLoanIds"; } diff --git a/fineract-e2e-tests-core/src/test/resources/fineract-test-application.properties b/fineract-e2e-tests-core/src/test/resources/fineract-test-application.properties index 65727e1431a..eb5ef6e178d 100644 --- a/fineract-e2e-tests-core/src/test/resources/fineract-test-application.properties +++ b/fineract-e2e-tests-core/src/test/resources/fineract-test-application.properties @@ -46,3 +46,10 @@ fineract-test.job.delay-in-ms=${POLLING_JOB_DELAY_IN_MS:3000} fineract-test.job.wait-timeout-in-ms=${POLLING_JOB_WAIT_TIMEOUT_IN_MS:120000} fineract-test.client-read-timeout=${CLIENT_READ_TIMEOUT:60} + +fineract-test.db.protocol=${TESTDB_PROTOCOL:jdbc:postgresql} +fineract-test.db.hostname=${TESTDB_HOSTNAME:localhost} +fineract-test.db.port=${TESTDB_PORT:5432} +fineract-test.db.name=${TESTDB_NAME:fineract_default} +fineract-test.db.username=${TESTDB_USERNAME:postgres} +fineract-test.db.password=${TESTDB_PASSWORD:skdcnwauicn2ucnaecasdsajdnizucawencascdca} diff --git a/fineract-e2e-tests-runner/build.gradle b/fineract-e2e-tests-runner/build.gradle index 223900d2403..333921e1f52 100644 --- a/fineract-e2e-tests-runner/build.gradle +++ b/fineract-e2e-tests-runner/build.gradle @@ -70,6 +70,10 @@ dependencies { testImplementation 'io.github.classgraph:classgraph:4.8.179' testImplementation 'org.apache.commons:commons-collections4:4.4' + + testImplementation 'org.springframework:spring-jdbc' + testImplementation 'org.postgresql:postgresql' + testImplementation 'org.mariadb.jdbc:mariadb-java-client' } tasks.named('test') { diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java new file mode 100644 index 00000000000..3a4433f9eca --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java @@ -0,0 +1,50 @@ +/** + * 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.test.initializer.global; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.JobBusinessStepConfigData; +import org.apache.fineract.test.helper.WorkFlowJobHelper; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class WcpCobBusinessStepInitializerStep implements FineractGlobalInitializerStep { + + private static final String WCP_COB_JOB_NAME = "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS"; + + private final WorkFlowJobHelper workFlowJobHelper; + + @Override + public void initialize() throws Exception { + try { + JobBusinessStepConfigData response = workFlowJobHelper.getConfiguredWorkflowSteps(WCP_COB_JOB_NAME); + log.info("WCP COB configured business steps: {}", response.getBusinessSteps()); + } catch (CallFailedRuntimeException e) { + log.warn("WCP COB business steps retrieval failed (expected if WCP COB not deployed): {}", e.getMessage()); + log.debug("Full stack trace:", e); + } + } +} diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature new file mode 100644 index 00000000000..21b1c805ab5 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature @@ -0,0 +1,142 @@ +@WCCOBFeature @WC +Feature: Working Capital COB Job + + Background: + Given Global configuration "enable-business-date" is enabled + + @TestRailId:C4695 + Scenario: Verify WC COB job registration, default business step, and scheduler metadata + Then Admin checks that configured business jobs contain "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" + Then Admin verifies configured business steps for "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" match: + | stepName | order | + | DUMMY_BUSINESS_STEP | 1 | + Then Admin verifies scheduler job "WC_COB" has display name "Working Capital Loan COB" + Then Admin verifies scheduler job "WC_COB" has active status "false" + + @TestRailId:C4696 + Scenario: WC COB and Loan COB coexistence — both jobs listed and execute without interference + Then Admin checks that configured business jobs contain "LOAN_CLOSE_OF_BUSINESS" + Then Admin checks that configured business jobs contain "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" + When Admin sets the business date to "01 January 2024" + When Admin runs COB job + When Admin runs WC COB job + + @TestRailId:C4697 + Scenario: WC COB executes on consecutive dates with Loan COB interleaved + When Admin sets the business date to "01 January 2024" + When Admin runs WC COB job + When Admin runs COB job + When Admin sets the business date to "02 January 2024" + When Admin runs WC COB job + + @TestRailId:CXXXX + Scenario: WC COB updates lastClosedBusinessDate for a single active loan + # Behavioral test: inserts a WC loan via JDBC, runs COB, verifies lastClosedBusinessDate is set. + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + + @TestRailId:CXXXX + Scenario: WC COB processes multiple loans in a single run + When Admin sets the business date to "01 January 2024" + Given Admin inserts 3 active WC loans into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + + @TestRailId:CXXXX + Scenario: WC COB advances lastClosedBusinessDate over consecutive business dates + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + When Admin sets the business date to "02 January 2024" + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "01 January 2024" + Then Admin verifies all inserted WC loans have version 2 + + @TestRailId:CXXXX + Scenario: WC COB does not reprocess loans already closed for the business date + # Verifies idempotency — running COB twice on the same business date doesn't cause errors or duplicate processing. + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + Then Admin verifies all inserted WC loans have version 1 + + @TestRailId:CXXXX + Scenario: WC COB skips loans with ineligible status (closed obligations met) + # COB only processes non-closed statuses: SUBMITTED_AND_PENDING_APPROVAL, APPROVED, ACTIVE, + # TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD. Closed loans should be skipped. + When Admin sets the business date to "01 January 2024" + Given Admin inserts a WC loan with status "CLOSED_OBLIGATIONS_MET" into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have null lastClosedBusinessDate + + @TestRailId:CXXXX + Scenario: WC COB processes loans with eligible non-active statuses + When Admin sets the business date to "01 January 2024" + Given Admin inserts a WC loan with status "SUBMITTED_AND_PENDING_APPROVAL" into the database + Given Admin inserts a WC loan with status "APPROVED" into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + + @TestRailId:CXXXX + Scenario: WC COB processes loans with transfer statuses + When Admin sets the business date to "01 January 2024" + Given Admin inserts a WC loan with status "TRANSFER_IN_PROGRESS" into the database + Given Admin inserts a WC loan with status "TRANSFER_ON_HOLD" into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + + @TestRailId:CXXXX + Scenario: WC COB skips loans already closed for the current business date + When Admin sets the business date to "01 January 2024" + Given Admin inserts a WC loan with status "ACTIVE" and lastClosedBusinessDate "31 December 2023" into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + Then Admin verifies all inserted WC loans have version 0 + + @TestRailId:CXXXX + Scenario: WC COB advances a loan that is exactly one day behind + When Admin sets the business date to "02 January 2024" + Given Admin inserts a WC loan with status "ACTIVE" and lastClosedBusinessDate "31 December 2023" into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "01 January 2024" + + @TestRailId:CXXXX + Scenario: WC COB releases all account locks after completion + # After COB completes, no lingering account locks should remain for processed loans. + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + Then Admin verifies all inserted WC loans have no account locks + + @TestRailId:CXXXX + Scenario: WC COB handles a batch of 10 loans + When Admin sets the business date to "01 January 2024" + Given Admin inserts 10 active WC loans into the database + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + + @TestRailId:CXXXX + Scenario: WC COB increments loan version after processing + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + Then Admin verifies all inserted WC loans have version 0 + When Admin runs WC COB job + Then Admin verifies all inserted WC loans have lastClosedBusinessDate "31 December 2023" + Then Admin verifies all inserted WC loans have version 1 + + @TestRailId:CXXXX + Scenario: WC COB processes eligible loans and skips ineligible ones in the same batch + # Mix of eligible (ACTIVE) and ineligible (CLOSED_OBLIGATIONS_MET) loans — only eligible should be updated. + When Admin sets the business date to "01 January 2024" + Given Admin inserts an active WC loan into the database + Given Admin inserts a WC loan with status "CLOSED_OBLIGATIONS_MET" into the database + When Admin runs WC COB job + Then Admin verifies inserted WC loan 1 has lastClosedBusinessDate "31 December 2023" + Then Admin verifies inserted WC loan 2 has null lastClosedBusinessDate diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ContextAwareTaskDecorator.java b/fineract-loan/src/main/java/org/apache/fineract/cob/loan/ContextAwareTaskDecorator.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/ContextAwareTaskDecorator.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/loan/ContextAwareTaskDecorator.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/cob/service/RetrieveLoanIdService.java b/fineract-loan/src/main/java/org/apache/fineract/cob/service/RetrieveLoanIdService.java new file mode 100644 index 00000000000..640368cbe11 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/cob/service/RetrieveLoanIdService.java @@ -0,0 +1,23 @@ +/** + * 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.service; + +public interface RetrieveLoanIdService extends RetrieveIdService { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java index 8c690ba68ce..4fc055b67be 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java @@ -38,7 +38,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.data.COBPartition; import org.apache.fineract.cob.loan.LoanCOBConstant; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.boot.FineractProfiles; @@ -63,7 +63,7 @@ public class InternalCOBApiResource implements InitializingBean { private static final String DATETIME_PATTERN = "dd MMMM yyyy"; - private final RetrieveLoanIdService retrieveLoanIdService; + private final RetrieveLoanIdService retrieveIdService; private final ApiRequestParameterHelper apiRequestParameterHelper; private final ToApiJsonSerializer toApiJsonSerializerForList; private final LoanRepositoryWrapper loanRepositoryWrapper; @@ -90,7 +90,7 @@ public void afterPropertiesSet() throws Exception { public String getCobPartitions(@Context final UriInfo uriInfo, @PathParam("partitionSize") int partitionSize) { LocalDate businessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.BUSINESS_DATE); log.info("RetrieveLoanCOBPartitions is called with partitionSize {} for {}", partitionSize, businessDate); - List loanCOBPartitions = retrieveLoanIdService.retrieveLoanCOBPartitions(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND, + List loanCOBPartitions = retrieveIdService.retrieveLoanCOBPartitions(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND, businessDate, false, partitionSize); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return toApiJsonSerializerForList.serialize(settings, loanCOBPartitions); diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java similarity index 98% rename from fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java rename to fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java index 98f2a0ae8eb..9453e59e86c 100644 --- a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java @@ -26,7 +26,7 @@ @Repository @RequiredArgsConstructor -public class CustomLoanAccountLockRepositoryImpl implements CustomLoanAccountLockRepository { +public class CustomLoanAccountLockRepositoryImpl implements CustomLoanAccountLockRepository { @PersistenceContext private EntityManager entityManager; @@ -52,4 +52,5 @@ and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING') entityManager.createNativeQuery(sql).executeUpdate(); entityManager.flush(); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java index 45f77d9b964..41faf7ef1ea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java @@ -18,63 +18,20 @@ */ package org.apache.fineract.cob.domain; -import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; import jakarta.persistence.Table; -import jakarta.persistence.Version; import java.time.LocalDate; -import java.time.OffsetDateTime; -import lombok.Getter; import lombok.NoArgsConstructor; -import org.apache.fineract.infrastructure.core.service.DateUtils; @Entity @Table(name = "m_loan_account_locks") @NoArgsConstructor -@Getter -public class LoanAccountLock { +public class LoanAccountLock extends AccountLock { - @Id - @Column(name = "loan_id", nullable = false) - private Long loanId; - - @Version - @Column(name = "version") - private Long version; - - @Enumerated(EnumType.STRING) - @Column(name = "lock_owner", nullable = false) - private LockOwner lockOwner; - - @Column(name = "lock_placed_on", nullable = false) - private OffsetDateTime lockPlacedOn; - - @Column(name = "error") - private String error; - - @Column(name = "stacktrace") - private String stacktrace; - - @Column(name = "lock_placed_on_cob_business_date") - private LocalDate lockPlacedOnCobBusinessDate; + private static final long serialVersionUID = 5267165818666471447L; public LoanAccountLock(Long loanId, LockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) { - this.loanId = loanId; - this.lockOwner = lockOwner; - this.lockPlacedOn = DateUtils.getAuditOffsetDateTime(); - this.lockPlacedOnCobBusinessDate = lockPlacedOnCobBusinessDate; - } - - public void setError(String errorMessage, String stacktrace) { - this.error = errorMessage; - this.stacktrace = stacktrace; + super(loanId, lockOwner, lockPlacedOnCobBusinessDate); } - public void setNewLockOwner(LockOwner newLockOwner) { - this.lockOwner = newLockOwner; - this.lockPlacedOn = DateUtils.getAuditOffsetDateTime(); - } } 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 08e908fad6b..3724185c46f 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 @@ -22,28 +22,31 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +@Repository public interface LoanAccountLockRepository - extends CustomLoanAccountLockRepository, JpaRepository, JpaSpecificationExecutor { + extends AccountLockRepository, JpaRepository, JpaSpecificationExecutor { + @Override Optional findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + @Override void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); + @Override List findAllByLoanIdIn(List loanIds); + @Override boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner); + @Override 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) - """) - @Modifying(flushAutomatically = true) - void removeLockByOwner(); - + @Override List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner); + + @Override + void removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(List lockOwners); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java index 818dd8fd6bb..a5cbe3f2ca4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java @@ -18,18 +18,23 @@ */ package org.apache.fineract.cob.listener; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.loan.LoanLockingService; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.transaction.support.TransactionTemplate; -public class ChunkProcessingLoanItemListener extends AbstractLoanItemListener { +@Slf4j +public class ChunkProcessingLoanItemListener extends AbstractLoanItemListener { - public ChunkProcessingLoanItemListener(LoanLockingService loanLockingService, TransactionTemplate transactionTemplate) { - super(loanLockingService, transactionTemplate); + public ChunkProcessingLoanItemListener(LockingService lockingService, TransactionTemplate transactionTemplate) { + super(lockingService, transactionTemplate); } @Override protected LockOwner getLockOwner() { return LockOwner.LOAN_COB_CHUNK_PROCESSING; } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java index 548c21328ff..7ad279804f1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java @@ -18,14 +18,16 @@ */ package org.apache.fineract.cob.listener; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.loan.LoanLockingService; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.transaction.support.TransactionTemplate; -public class InlineCOBLoanItemListener extends AbstractLoanItemListener { +public class InlineCOBLoanItemListener extends AbstractLoanItemListener { - public InlineCOBLoanItemListener(LoanLockingService loanLockingService, TransactionTemplate transactionTemplate) { - super(loanLockingService, transactionTemplate); + public InlineCOBLoanItemListener(LockingService lockingService, TransactionTemplate transactionTemplate) { + super(lockingService, transactionTemplate); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java new file mode 100644 index 00000000000..471a60253fb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java @@ -0,0 +1,46 @@ +/** + * 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.listener; + +import org.apache.fineract.cob.conditions.BatchWorkerCondition; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; + +@Component +@Conditional(BatchWorkerCondition.class) +public class WorkingCapitalChunkProcessingLoanItemListener + extends AbstractLoanItemListener { + + public WorkingCapitalChunkProcessingLoanItemListener( + LockingService workingCapitalLoanAccountLockLockingService, + TransactionTemplate transactionTemplate) { + super(workingCapitalLoanAccountLockLockingService, transactionTemplate); + } + + @Override + protected LockOwner getLockOwner() { + return LockOwner.LOAN_COB_CHUNK_PROCESSING; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java index 42f140b3a6e..8a5ae95ec44 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java @@ -18,55 +18,29 @@ */ package org.apache.fineract.cob.loan; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.stream.Collectors; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.COBBusinessStepService; -import org.apache.fineract.cob.data.BusinessStepNameAndOrder; +import org.apache.fineract.cob.processor.AbstractItemProcessor; 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.StepExecution; -import org.springframework.batch.core.annotation.AfterStep; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemProcessor; import org.springframework.lang.NonNull; -@RequiredArgsConstructor -@Slf4j -public abstract class AbstractLoanItemProcessor implements ItemProcessor { +public abstract class AbstractLoanItemProcessor extends AbstractItemProcessor { - private final COBBusinessStepService cobBusinessStepService; private final ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; - @Setter(AccessLevel.PROTECTED) - private ExecutionContext executionContext; - private LocalDate businessDate; + public AbstractLoanItemProcessor(COBBusinessStepService cobBusinessStepService, + ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) { + super(cobBusinessStepService); + this.progressiveLoanModelProcessingService = progressiveLoanModelProcessingService; + } - @SuppressWarnings({ "unchecked" }) @Override public Loan process(@NonNull Loan loan) throws Exception { if (needToRebuildModel(loan)) { progressiveLoanModelProcessingService.recalculateModelAndSave(loan.getId()); } - Set businessSteps = (Set) executionContext.get(LoanCOBConstant.BUSINESS_STEPS); - if (businessSteps == null) { - throw new IllegalStateException("No business steps found in the execution context"); - } - TreeMap businessStepMap = getBusinessStepMap(businessSteps); - - Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, loan); - alreadyProcessedLoan.setLastClosedBusinessDate(businessDate); - return alreadyProcessedLoan; + return super.process(loan); } private boolean needToRebuildModel(Loan loan) { @@ -74,22 +48,9 @@ private boolean needToRebuildModel(Loan loan) { ProgressiveLoanInterestScheduleModel.getModelVersion()); } - private TreeMap getBusinessStepMap(Set businessSteps) { - Map businessStepMap = businessSteps.stream() - .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName)); - return new TreeMap<>(businessStepMap); - } - - @AfterStep - public ExitStatus afterStep(@NonNull StepExecution stepExecution) { - return ExitStatus.COMPLETED; - } - - protected void setBusinessDate(StepExecution stepExecution) { - this.businessDate = LocalDate.parse( - Objects.requireNonNull( - (String) stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)), - DateTimeFormatter.ISO_DATE); + @Override + public void setLastRun(Loan processedLoan) { + processedLoan.setLastClosedBusinessDate(getBusinessDate()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java index 759c2add2d7..c9ad1c5b44f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java @@ -23,33 +23,33 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.cob.exceptions.LoanReadException; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.cob.exceptions.LockedReadException; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.AfterStep; import org.springframework.batch.item.ItemReader; +import org.springframework.data.repository.CrudRepository; import org.springframework.lang.NonNull; @Slf4j @RequiredArgsConstructor -public abstract class AbstractLoanItemReader implements ItemReader { +public abstract class AbstractLoanItemReader> implements ItemReader { - protected final LoanRepository loanRepository; + protected final CrudRepository loanRepository; @Setter(AccessLevel.PROTECTED) private LinkedBlockingQueue remainingData; @Override - public Loan read() throws Exception { + public T read() throws Exception { final Long loanId = remainingData.poll(); if (loanId != null) { try { return loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); } catch (Exception e) { - throw new LoanReadException(loanId, e); + throw new LockedReadException(loanId, e); } } return null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java index f7e2b60bf37..9518ffef960 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java @@ -21,7 +21,9 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.batch.item.Chunk; @@ -32,7 +34,7 @@ @RequiredArgsConstructor public abstract class AbstractLoanItemWriter extends RepositoryItemWriter { - private final LoanLockingService loanLockingService; + private final LockingService loanLockingService; @Override public void write(@NonNull Chunk items) throws Exception { diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java index b8eda2d144f..01daf033532 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java @@ -18,95 +18,31 @@ */ package org.apache.fineract.cob.loan; -import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW; - -import com.google.common.collect.Lists; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.cob.converter.COBParameterConverter; -import org.apache.fineract.cob.data.COBParameter; +import org.apache.fineract.cob.COBConstant; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.exceptions.LoanLockCannotBeAppliedException; -import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.apache.fineract.cob.tasklet.ApplyCommonLockTasklet; import org.apache.fineract.infrastructure.core.config.FineractProperties; -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.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; @Slf4j -@RequiredArgsConstructor -public class ApplyLoanLockTasklet implements Tasklet { - - private static final long NUMBER_OF_RETRIES = 3; - private final FineractProperties fineractProperties; - private final LoanLockingService loanLockingService; - private final RetrieveLoanIdService retrieveLoanIdService; - private final TransactionTemplate transactionTemplate; - - @Override - @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") - public RepeatStatus execute(@NonNull StepContribution contribution, @NonNull ChunkContext chunkContext) - throws LoanLockCannotBeAppliedException { - ExecutionContext executionContext = contribution.getStepExecution().getExecutionContext(); - long numberOfExecutions = contribution.getStepExecution().getCommitCount(); - COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER)); - boolean isCatchUp = CatchUpFlagResolver.resolve(contribution.getStepExecution()); - List loanIds; - if (Objects.isNull(loanCOBParameter) - || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId())) - || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) { - loanIds = Collections.emptyList(); - } else { - loanIds = new ArrayList<>( - retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp)); - } - List> loanIdPartitions = Lists.partition(loanIds, getInClauseParameterSizeLimit()); - List accountLocks = new ArrayList<>(); - loanIdPartitions.forEach(loanIdPartition -> accountLocks.addAll(loanLockingService.findAllByLoanIdIn(loanIdPartition))); - - List toBeProcessedLoanIds = new ArrayList<>(loanIds); - List alreadyLockedAccountIds = accountLocks.stream().map(LoanAccountLock::getLoanId).toList(); +public class ApplyLoanLockTasklet extends ApplyCommonLockTasklet { - toBeProcessedLoanIds.removeAll(alreadyLockedAccountIds); - try { - applyLocks(toBeProcessedLoanIds); - } catch (Exception e) { - if (numberOfExecutions > NUMBER_OF_RETRIES) { - String message = "There was an error applying lock to loan accounts."; - log.error("{}", message, e); - throw new LoanLockCannotBeAppliedException(message, e); - } else { - return RepeatStatus.CONTINUABLE; - } - } - - return RepeatStatus.FINISHED; + public ApplyLoanLockTasklet(FineractProperties fineractProperties, LockingService loanLockingService, + RetrieveIdService retrieveIdService, TransactionTemplate transactionTemplate) { + super(fineractProperties, loanLockingService, retrieveIdService, transactionTemplate); } - private void applyLocks(List toBeProcessedLoanIds) { - transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW); - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) { - loanLockingService.applyLock(toBeProcessedLoanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING); - } - }); + @Override + public String getCOBParameter() { + return COBConstant.COB_PARAMETER; } - private int getInClauseParameterSizeLimit() { - return fineractProperties.getQuery().getInClauseParameterSizeLimit(); + @Override + public LockOwner getLockOwner() { + return LockOwner.LOAN_COB_CHUNK_PROCESSING; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java index c25a9c33f97..fc33af0c439 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java @@ -20,13 +20,14 @@ import java.util.List; import java.util.concurrent.LinkedBlockingQueue; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.BeforeStep; import org.springframework.batch.item.ExecutionContext; import org.springframework.lang.NonNull; -public class InlineCOBLoanItemReader extends AbstractLoanItemReader { +public class InlineCOBLoanItemReader extends AbstractLoanItemReader { public InlineCOBLoanItemReader(LoanRepository loanRepository) { super(loanRepository); @@ -36,7 +37,7 @@ public InlineCOBLoanItemReader(LoanRepository loanRepository) { @SuppressWarnings({ "unchecked" }) public void beforeStep(@NonNull StepExecution stepExecution) { ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext(); - List loanIds = (List) executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER); + List loanIds = (List) executionContext.get(LoanCOBConstant.COB_PARAMETER); setRemainingData(new LinkedBlockingQueue<>(loanIds)); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java index 9a839d32688..07b8acb84c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java @@ -18,11 +18,13 @@ */ package org.apache.fineract.cob.loan; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; public class InlineCOBLoanItemWriter extends AbstractLoanItemWriter { - public InlineCOBLoanItemWriter(LoanLockingService loanLockingService) { + public InlineCOBLoanItemWriter(LockingService loanLockingService) { super(loanLockingService); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java index 400a3589308..28626e226d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java @@ -63,8 +63,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon ThreadLocalContextUtil.setActionContext(ActionContext.COB); Set cobBusinessSteps = cobBusinessStepService.getCOBBusinessSteps(LoanCOBBusinessStep.class, LoanCOBConstant.LOAN_COB_JOB_NAME); - contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.LOAN_COB_PARAMETER, - getLoanIdsFromJobParameters(chunkContext)); + contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.COB_PARAMETER, getLoanIdsFromJobParameters(chunkContext)); contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_STEPS, cobBusinessSteps); String businessDateString = getBusinessDateFromJobParameters(chunkContext); contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDateString); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java index ad21d3fcf9d..f9d993a6f29 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java @@ -25,14 +25,12 @@ public final class LoanCOBConstant extends COBConstant { public static final String JOB_NAME = "LOAN_COB"; public static final String JOB_HUMAN_READABLE_NAME = "Loan COB"; public static final String LOAN_COB_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS"; - public static final String LOAN_COB_PARAMETER = "loanCobParameter"; public static final String LOAN_COB_WORKER_STEP = "loanCOBWorkerStep"; public static final String INLINE_LOAN_COB_JOB_NAME = "INLINE_LOAN_COB"; public static final String LOAN_IDS_PARAMETER_NAME = "LoanIds"; public static final String LOAN_COB_PARTITIONER_STEP = "Loan COB partition - Step"; - public static final String PARTITION_KEY = "partition"; private LoanCOBConstant() { 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 61b08ba21b6..c81e92a44e5 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 @@ -24,6 +24,7 @@ import org.apache.fineract.cob.common.CustomJobParameterResolver; import org.apache.fineract.cob.conditions.BatchManagerCondition; import org.apache.fineract.cob.listener.COBExecutionListenerRunner; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.springbatch.PropertyService; @@ -70,7 +71,7 @@ public class LoanCOBManagerConfiguration { @Autowired private ApplicationContext applicationContext; @Autowired - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveLoanIdService retrieveIdService; @Autowired private BusinessEventNotifierService businessEventNotifierService; @Autowired @@ -79,7 +80,7 @@ public class LoanCOBManagerConfiguration { @Bean @StepScope public LoanCOBPartitioner partitioner(@Value("#{stepExecution}") StepExecution stepExecution) { - return new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveLoanIdService, jobOperator, stepExecution, + return new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveIdService, jobOperator, stepExecution, LoanCOBConstant.NUMBER_OF_DAYS_BEHIND); } @@ -109,7 +110,7 @@ public ResolveLoanCOBCustomJobParametersTasklet resolveCustomJobParametersTaskle @Bean public StayedLockedLoansTasklet stayedLockedTasklet() { - return new StayedLockedLoansTasklet(businessEventNotifierService, retrieveLoanIdService); + return new StayedLockedLoansTasklet(businessEventNotifierService, retrieveIdService); } @Bean(name = "loanCOBJob") @@ -117,7 +118,8 @@ public Job loanCOBJob(LoanCOBPartitioner partitioner) { return new JobBuilder(JobName.LOAN_COB.name(), jobRepository) // .listener(new COBExecutionListenerRunner(applicationContext, JobName.LOAN_COB.name())) // .start(resolveCustomJobParametersStep()) // - .next(loanCOBStep(partitioner)).next(stayedLockedStep()) // + .next(loanCOBStep(partitioner)) // + .next(stayedLockedStep()) // .incrementer(new RunIdIncrementer()) // .build(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java index b3cfdbc65d9..9bd2559f348 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java @@ -18,42 +18,33 @@ */ package org.apache.fineract.cob.loan; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.common.CommonPartitioner; import org.apache.fineract.cob.data.BusinessStepNameAndOrder; -import org.apache.fineract.cob.data.COBParameter; -import org.apache.fineract.cob.data.COBPartition; -import org.apache.fineract.cob.resolver.BusinessDateResolver; -import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.springframework.batch.core.StepExecution; -import org.springframework.batch.core.launch.JobExecutionNotRunningException; import org.springframework.batch.core.launch.JobOperator; -import org.springframework.batch.core.launch.NoSuchJobExecutionException; import org.springframework.batch.core.partition.support.Partitioner; import org.springframework.batch.item.ExecutionContext; import org.springframework.lang.NonNull; -import org.springframework.util.StopWatch; @Slf4j -@RequiredArgsConstructor -public class LoanCOBPartitioner implements Partitioner { - - public static final String PARTITION_PREFIX = "partition_"; +public class LoanCOBPartitioner extends CommonPartitioner implements Partitioner { private final PropertyService propertyService; private final COBBusinessStepService cobBusinessStepService; - private final RetrieveLoanIdService retrieveLoanIdService; - private final JobOperator jobOperator; - private final StepExecution stepExecution; - private final Long numberOfDays; + + public LoanCOBPartitioner(PropertyService propertyService, COBBusinessStepService cobBusinessStepService, + RetrieveIdService retrieveIdService, JobOperator jobOperator, StepExecution stepExecution, Long numberOfDaysBehind) { + super(jobOperator, stepExecution, numberOfDaysBehind, retrieveIdService); + this.propertyService = propertyService; + this.cobBusinessStepService = cobBusinessStepService; + + } @NonNull @Override @@ -64,54 +55,4 @@ public Map partition(int gridSize) { return getPartitions(partitionSize, cobBusinessSteps); } - private Map getPartitions(int partitionSize, Set cobBusinessSteps) { - if (cobBusinessSteps.isEmpty()) { - stopJobExecution(); - return Map.of(); - } - LocalDate businessDate = BusinessDateResolver.resolve(stepExecution); - boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution); - StopWatch sw = new StopWatch(); - sw.start(); - List loanCOBPartitions = new ArrayList<>( - retrieveLoanIdService.retrieveLoanCOBPartitions(numberOfDays, businessDate, isCatchUp, partitionSize)); - sw.stop(); - // if there is no loan to be closed, we still would like to create at least one partition - - if (loanCOBPartitions.isEmpty()) { - loanCOBPartitions.add(new COBPartition(0L, 0L, 1L, 0L)); - } - log.info( - "LoanCOBPartitioner found {} loans to be processed as part of COB. {} partitions were created using partition size {}. RetrieveLoanCOBPartitions was executed in {} ms.", - getLoanCount(loanCOBPartitions), loanCOBPartitions.size(), partitionSize, sw.getTotalTimeMillis()); - return loanCOBPartitions.stream().collect(Collectors.toMap(l -> PARTITION_PREFIX + l.getPageNo(), - l -> createNewPartition(cobBusinessSteps, l, businessDate, isCatchUp))); - } - - private long getLoanCount(List loanCOBPartitions) { - return loanCOBPartitions.stream().map(COBPartition::getCount).reduce(0L, Long::sum); - } - - private ExecutionContext createNewPartition(Set cobBusinessSteps, COBPartition loanCOBPartition, - LocalDate businessDate, boolean isCatchUp) { - ExecutionContext executionContext = new ExecutionContext(); - executionContext.put(LoanCOBConstant.BUSINESS_STEPS, cobBusinessSteps); - executionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER, - new COBParameter(loanCOBPartition.getMinId(), loanCOBPartition.getMaxId())); - executionContext.put(LoanCOBConstant.PARTITION_KEY, PARTITION_PREFIX + loanCOBPartition.getPageNo()); - executionContext.put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDate.toString()); - executionContext.put(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, Boolean.toString(isCatchUp)); - return executionContext; - } - - private void stopJobExecution() { - Long jobId = stepExecution.getJobExecution().getId(); - try { - jobOperator.stop(jobId); - } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) { - log.error("There is no running execution for the given execution ID. Execution ID: {}", jobId); - throw new RuntimeException(e); - } - - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java index 17d97f57090..b84b8001593 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java @@ -22,7 +22,11 @@ import org.apache.fineract.cob.common.InitialisationTasklet; import org.apache.fineract.cob.common.ResetContextTasklet; import org.apache.fineract.cob.conditions.BatchWorkerCondition; +import org.apache.fineract.cob.domain.LoanAccountLock; +import org.apache.fineract.cob.domain.LockingService; import org.apache.fineract.cob.listener.ChunkProcessingLoanItemListener; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.springbatch.PropertyService; @@ -74,12 +78,12 @@ public class LoanCOBWorkerConfiguration { @Autowired private TransactionTemplate transactionTemplate; @Autowired - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveLoanIdService retrieveIdService; @Autowired private FineractProperties fineractProperties; @Autowired - private LoanLockingService loanLockingService; + private LockingService loanLockingService; @Autowired private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; @@ -165,7 +169,7 @@ public ChunkProcessingLoanItemListener loanItemListener() { @Bean public ApplyLoanLockTasklet applyLock() { - return new ApplyLoanLockTasklet(fineractProperties, loanLockingService, retrieveLoanIdService, transactionTemplate); + return new ApplyLoanLockTasklet(fineractProperties, loanLockingService, retrieveIdService, transactionTemplate); } @Bean @@ -176,7 +180,7 @@ public ResetContextTasklet resetContext() { @Bean @StepScope public LoanItemReader cobWorkerItemReader() { - return new LoanItemReader(loanRepository, retrieveLoanIdService, loanLockingService); + return new LoanItemReader(loanRepository, new BeforeStepLockingItemReaderHelper<>(retrieveIdService, loanLockingService)); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java index bd00a20ad3e..b84be1e205e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java @@ -22,6 +22,8 @@ import org.apache.fineract.cob.common.CustomJobParameterResolver; import org.apache.fineract.cob.common.ResetContextTasklet; import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; +import org.apache.fineract.cob.domain.LoanAccountLock; +import org.apache.fineract.cob.domain.LockingService; import org.apache.fineract.cob.listener.InlineCOBLoanItemListener; import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository; import org.apache.fineract.infrastructure.jobs.service.JobName; @@ -67,7 +69,7 @@ public class LoanInlineCOBConfig { @Autowired private CustomJobParameterResolver customJobParameterResolver; @Autowired - private LoanLockingService loanLockingService; + private LockingService loanLockingService; @Autowired private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService; @@ -136,7 +138,7 @@ public ResetContextTasklet inlineCOBResetContext() { @Bean public ExecutionContextPromotionListener inlineCobPromotionListener() { ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener(); - listener.setKeys(new String[] { LoanCOBConstant.LOAN_COB_PARAMETER, LoanCOBConstant.BUSINESS_STEPS, + listener.setKeys(new String[] { LoanCOBConstant.COB_PARAMETER, LoanCOBConstant.BUSINESS_STEPS, LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME }); return listener; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java index f6c52f2df1c..0305897ea50 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java @@ -18,61 +18,29 @@ */ package org.apache.fineract.cob.loan; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.LinkedBlockingQueue; import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.cob.converter.COBParameterConverter; -import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.domain.LoanAccountLock; -import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.resolver.CatchUpFlagResolver; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.annotation.BeforeStep; -import org.springframework.batch.item.ExecutionContext; import org.springframework.lang.NonNull; @Slf4j -public class LoanItemReader extends AbstractLoanItemReader { +public class LoanItemReader extends AbstractLoanItemReader { - private final RetrieveLoanIdService retrieveLoanIdService; - private final LoanLockingService loanLockingService; + private final BeforeStepLockingItemReaderHelper beforeStepLockingItemReaderHelper; - public LoanItemReader(LoanRepository loanRepository, RetrieveLoanIdService retrieveLoanIdService, - LoanLockingService loanLockingService) { + public LoanItemReader(LoanRepository loanRepository, + BeforeStepLockingItemReaderHelper beforeStepLockingItemReaderHelper) { super(loanRepository); - this.retrieveLoanIdService = retrieveLoanIdService; - this.loanLockingService = loanLockingService; + this.beforeStepLockingItemReaderHelper = beforeStepLockingItemReaderHelper; } @BeforeStep - @SuppressWarnings({ "unchecked" }) public void beforeStep(@NonNull StepExecution stepExecution) { - ExecutionContext executionContext = stepExecution.getExecutionContext(); - COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER)); - List loanIds; - boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution); - if (Objects.isNull(loanCOBParameter) - || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId())) - || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) { - loanIds = Collections.emptyList(); - } else { - loanIds = retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, - isCatchUp); - if (!loanIds.isEmpty()) { - List lockedByCOBChunkProcessingAccountIds = getLoanIdsLockedWithChunkProcessingLock(loanIds); - loanIds.retainAll(lockedByCOBChunkProcessingAccountIds); - } - } - setRemainingData(new LinkedBlockingQueue<>(loanIds)); + setRemainingData(beforeStepLockingItemReaderHelper.filterRemainingData(stepExecution)); } - private List getLoanIdsLockedWithChunkProcessingLock(List loanIds) { - List accountLocks = new ArrayList<>( - loanLockingService.findAllByLoanIdInAndLockOwner(loanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING)); - return accountLocks.stream().map(LoanAccountLock::getLoanId).toList(); - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java index 2696ee03a13..28c83148e13 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java @@ -18,11 +18,13 @@ */ package org.apache.fineract.cob.loan; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; public class LoanItemWriter extends AbstractLoanItemWriter { - public LoanItemWriter(LoanLockingService loanLockingService) { + public LoanItemWriter(LockingService loanLockingService) { super(loanLockingService); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java index 660051907fe..91dae8c06d8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java @@ -18,7 +18,9 @@ */ package org.apache.fineract.cob.loan; +import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LoanAccountLockRepository; +import org.apache.fineract.cob.domain.LockingService; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -38,7 +40,7 @@ public class LoanLockingConfiguration { @Bean @ConditionalOnMissingBean - public LoanLockingService retrieveLoanLockingService() { + public LockingService retrieveLoanLockingService() { return new LoanLockingServiceImpl(jdbcTemplate, fineractProperties, loanAccountLockRepository); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java index 5a90c781f4a..18a47a547c1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java @@ -18,79 +18,36 @@ */ package org.apache.fineract.cob.loan; -import java.sql.PreparedStatement; -import java.time.LocalDate; -import java.util.List; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.AbstractLockingService; import org.apache.fineract.cob.domain.LoanAccountLock; 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.config.FineractProperties; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.springframework.jdbc.core.JdbcTemplate; -@RequiredArgsConstructor @Slf4j -public class LoanLockingServiceImpl implements LoanLockingService { +public class LoanLockingServiceImpl extends AbstractLockingService { private static final String BATCH_LOAN_LOCK_INSERT = """ INSERT INTO m_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date) VALUES (?,?,?,?,?) """; - private final JdbcTemplate jdbcTemplate; - private final FineractProperties fineractProperties; - private final LoanAccountLockRepository loanAccountLockRepository; - - @Override - public void upgradeLock(List accountsToLock, LockOwner lockOwner) { - jdbcTemplate.batchUpdate(""" - UPDATE m_loan_account_locks SET version= version + 1, lock_owner = ?, lock_placed_on = ? WHERE loan_id = ? - """, accountsToLock, getInClauseParameterSizeLimit(), (ps, id) -> { - ps.setString(1, lockOwner.name()); - ps.setObject(2, DateUtils.getAuditOffsetDateTime()); - ps.setLong(3, id); - }); - } - - @Override - public List findAllByLoanIdIn(List loanIds) { - return loanAccountLockRepository.findAllByLoanIdIn(loanIds); - } - - @Override - public LoanAccountLock findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner) { - return loanAccountLockRepository.findByLoanIdAndLockOwner(loanId, lockOwner).orElseGet(() -> { - log.warn("There is no lock for loan account with id: {}", loanId); - return null; - }); - } + private static final String BATCH_LOAN_LOCK_UPGRADE = """ + UPDATE m_loan_account_locks SET version= version + 1, lock_owner = ?, lock_placed_on = ? WHERE loan_id = ? + """; - @Override - public List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) { - return loanAccountLockRepository.findAllByLoanIdInAndLockOwner(loanIds, lockOwner); + public LoanLockingServiceImpl(JdbcTemplate jdbcTemplate, FineractProperties fineractProperties, + LoanAccountLockRepository loanAccountLockRepository) { + super(jdbcTemplate, fineractProperties, loanAccountLockRepository); } @Override - public void applyLock(List loanIds, LockOwner lockOwner) { - LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); - jdbcTemplate.batchUpdate(BATCH_LOAN_LOCK_INSERT, loanIds, loanIds.size(), (PreparedStatement ps, Long loanId) -> { - ps.setLong(1, loanId); - ps.setLong(2, 1); - ps.setString(3, lockOwner.name()); - ps.setObject(4, DateUtils.getAuditOffsetDateTime()); - ps.setObject(5, cobBusinessDate); - }); + protected String getBatchLoanLockUpgrade() { + return BATCH_LOAN_LOCK_UPGRADE; } @Override - public void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) { - loanAccountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, lockOwner); - } - - private int getInClauseParameterSizeLimit() { - return fineractProperties.getQuery().getInClauseParameterSizeLimit(); + protected String getBatchLoanLockInsert() { + return BATCH_LOAN_LOCK_INSERT; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java similarity index 93% rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java rename to fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java index cdc09d38d5b..8fa29493dfc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java @@ -18,8 +18,6 @@ */ package org.apache.fineract.cob.loan; -import java.sql.ResultSet; -import java.sql.SQLException; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +28,8 @@ import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.data.COBPartition; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; @@ -38,7 +38,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @RequiredArgsConstructor -public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdService { +public class RetrieveAllNonClosedIdServiceImpl implements RetrieveLoanIdService { private static final Collection NON_CLOSED_LOAN_STATUSES = new ArrayList<>( Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED, LoanStatus.ACTIVE, @@ -67,11 +67,7 @@ public List retrieveLoanCOBPartitions(Long numberOfDays, LocalDate parameters.addValue("pageSize", partitionSize); parameters.addValue("statusIds", List.of(100, 200, 300, 303, 304)); parameters.addValue("businessDate", businessDate.minusDays(numberOfDays)); - return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveAllNonClosedLoanIdServiceImpl::mapRow); - } - - private static COBPartition mapRow(ResultSet rs, int rowNum) throws SQLException { - return new COBPartition(rs.getLong("min"), rs.getLong("max"), rs.getLong("page"), rs.getLong("count")); + return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveIdService::mapRow); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java index 43aa60e6e15..1c18aa52412 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.cob.loan; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -37,6 +38,6 @@ public class RetrieveLoanIdConfiguration { @Bean @ConditionalOnMissingBean public RetrieveLoanIdService retrieveLoanIdService() { - return new RetrieveAllNonClosedLoanIdServiceImpl(loanRepository, namedParameterJdbcTemplate); + return new RetrieveAllNonClosedIdServiceImpl(loanRepository, namedParameterJdbcTemplate); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java index 2bc4f773b90..b23e58148f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java @@ -26,6 +26,7 @@ import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo; import org.apache.fineract.cob.data.LoanAccountStayedLockedData; import org.apache.fineract.cob.data.LoanAccountsStayedLockedData; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; @@ -39,7 +40,7 @@ public class StayedLockedLoansTasklet implements Tasklet { private final BusinessEventNotifierService businessEventNotifierService; - private final RetrieveLoanIdService retrieveLoanIdService; + private final RetrieveIdService retrieveIdService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -52,7 +53,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon private LoanAccountsStayedLockedData buildLoanAccountData() { LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); - List stayedLockedLoanAccounts = retrieveLoanIdService + List stayedLockedLoanAccounts = retrieveIdService .findAllStayedLockedByCobBusinessDate(cobBusinessDate); List loanAccounts = new ArrayList<>(); stayedLockedLoanAccounts.forEach(loanAccount -> { diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java index 8de65836790..a8a6c35a18d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java @@ -29,7 +29,6 @@ import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; import org.apache.fineract.cob.loan.LoanCOBConstant; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.config.TaskExecutorConstant; import org.apache.fineract.infrastructure.core.domain.FineractContext; @@ -62,7 +61,7 @@ public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorServ private final JobLocator jobLocator; private final ScheduledJobDetailRepository scheduledJobDetailRepository; private final JobStarter jobStarter; - private final RetrieveLoanIdService retrieveLoanIdService; + private final RetrieveLoanIdService retrieveIdService; @Override @Async(TaskExecutorConstant.LOAN_COB_CATCH_UP_TASK_EXECUTOR_BEAN_NAME) @@ -70,7 +69,7 @@ public void executeLoanCOBCatchUpAsync(FineractContext context) { try { ThreadLocalContextUtil.init(context); LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); - List loanIdAndLastClosedBusinessDate = retrieveLoanIdService + List loanIdAndLastClosedBusinessDate = retrieveIdService .retrieveLoanIdsOldestCobProcessed(cobBusinessDate); LocalDate oldestCOBProcessedDate = !loanIdAndLastClosedBusinessDate.isEmpty() diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java index 98d9def4bab..ad2b8bc84e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java @@ -43,7 +43,6 @@ import org.apache.fineract.cob.domain.LockOwner; import org.apache.fineract.cob.exceptions.AccountLockCannotBeOverruledException; import org.apache.fineract.cob.loan.LoanCOBConstant; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -94,7 +93,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService getLoansToBeProcessed(List lo List loanIdAndLastClosedBusinessDates = new ArrayList<>(); List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit()); partitions.forEach(partition -> loanIdAndLastClosedBusinessDates - .addAll(retrieveLoanIdService.retrieveLoanIdsBehindDateOrNull(cobBusinessDate, partition))); + .addAll(retrieveIdService.retrieveLoanIdsBehindDateOrNull(cobBusinessDate, partition))); return loanIdAndLastClosedBusinessDates; } 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 a2bc44816c5..1e89ee8e402 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 @@ -18,16 +18,16 @@ */ package org.apache.fineract.cob.service; -import java.util.List; +import org.apache.fineract.cob.domain.AccountLockRepository; +import org.apache.fineract.cob.domain.CustomLoanAccountLockRepository; import org.apache.fineract.cob.domain.LoanAccountLock; +import org.springframework.stereotype.Service; -public interface LoanAccountLockService { +@Service +public class LoanAccountLockService extends AbstractAccountLockService { - List getLockedLoanAccountByPage(int page, int limit); - - boolean isLoanHardLocked(Long loanId); - - boolean isLockOverrulable(Long loanId); - - void updateCobAndRemoveLocks(); + public LoanAccountLockService(AccountLockRepository loanAccountLockRepository, + CustomLoanAccountLockRepository customLoanAccountLockRepository) { + super(loanAccountLockRepository, customLoanAccountLockRepository); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java index 39b346b4a3a..85dd2f3b297 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java @@ -26,7 +26,6 @@ import org.apache.fineract.cob.data.IsCatchUpRunningDTO; import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO; import org.apache.fineract.cob.loan.LoanCOBConstant; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.FineractContext; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; @@ -41,7 +40,7 @@ public class LoanCOBCatchUpServiceImpl implements LoanCOBCatchUpService { private final AsyncLoanCOBExecutorService asyncLoanCOBExecutorService; private final JobExecutionRepository jobExecutionRepository; - private final RetrieveLoanIdService retrieveLoanIdService; + private final RetrieveLoanIdService retrieveIdService; private final LoanAccountLockService accountLockService; @Override @@ -51,7 +50,7 @@ public void unlockHardLockedLoans() { @Override public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() { - List loanIdAndLastClosedBusinessDate = retrieveLoanIdService + List loanIdAndLastClosedBusinessDate = retrieveIdService .retrieveLoanIdsOldestCobProcessed(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)); OldestCOBProcessedLoanDTO oldestCOBProcessedLoanDTO = new OldestCOBProcessedLoanDTO(); oldestCOBProcessedLoanDTO.setLoanIds(loanIdAndLastClosedBusinessDate.stream().map(COBIdAndLastClosedBusinessDate::getId).toList()); 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 d1d79484216..640fd053c0f 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 @@ -41,9 +41,9 @@ import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; import org.apache.fineract.cob.loan.LoanCOBConstant; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl; import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -72,7 +72,7 @@ public class LoanCOBFilterHelper implements InitializingBean { private final InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService; private final LoanRepository loanRepository; private final FineractProperties fineractProperties; - private final RetrieveLoanIdService retrieveLoanIdService; + private final RetrieveLoanIdService retrieveIdService; private final LoanRescheduleRequestRepository loanRescheduleRequestRepository; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -197,9 +197,9 @@ public boolean isLoanBehind(List loanIds) { List loanIdAndLastClosedBusinessDates = new ArrayList<>(); List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit()); partitions.forEach(partition -> { - loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService + loanIdAndLastClosedBusinessDates.addAll(retrieveIdService .retrieveLoanIdsBehindDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)); - loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService.retrieveLoanBehindOnDisbursementDate( + loanIdAndLastClosedBusinessDates.addAll(retrieveIdService.retrieveLoanBehindOnDisbursementDate( ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)); }); return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java index a2ee619b06a..ec1013d04f3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java @@ -18,12 +18,14 @@ */ package org.apache.fineract.infrastructure.jobs.service.jobparameterprovider; +import java.util.List; + public abstract class AbstractJobParameterProvider implements JobParameterProvider { @Override public boolean canProvideParametersForJob(String jobName) { - return jobName.equals(getJobName()); + return getJobNames().contains(jobName); } - protected abstract String getJobName(); + protected abstract List getJobNames(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java index 557aaeb234a..554f4a8837a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java @@ -21,6 +21,7 @@ import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -52,8 +53,8 @@ public Map> provide(Set jobParameter } @Override - public String getJobName() { - return JobName.LOAN_COB.name(); + protected List getJobNames() { + return List.of(JobName.LOAN_COB.name(), JobName.WORKING_CAPITAL_LOAN_COB_JOB.name()); } private Set getJobParameterDTOListWithCorrectBusinessDate(Set jobParameterDTOset) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 066de64b964..c90c9e2a792 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -52,7 +52,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.cob.exceptions.AccountLockCannotBeOverruledException; -import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.cob.service.AccountLockService; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -267,7 +267,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final PostDatedChecksRepository postDatedChecksRepository; private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; private final LoanLifecycleStateMachine loanLifecycleStateMachine; - private final LoanAccountLockService loanAccountLockService; + private final AccountLockService loanAccountLockService; private final ExternalIdFactory externalIdFactory; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final ErrorHandler errorHandler; diff --git a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml index a2d2eef8175..9fe5069589f 100644 --- a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml +++ b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml @@ -280,6 +280,7 @@ org.apache.fineract.portfolio.account.domain.AccountTransferTransaction + org.apache.fineract.notification.domain.NotificationMapper org.apache.fineract.notification.domain.Notification @@ -288,7 +289,6 @@ org.apache.fineract.adhocquery.domain.AdHoc - org.apache.fineract.cob.domain.LoanAccountLock org.apache.fineract.cob.domain.BatchBusinessStep diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java index ba559cbc8b9..b857cdb70dc 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java @@ -32,8 +32,8 @@ import java.util.List; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.exceptions.LoanReadException; -import org.apache.fineract.cob.loan.LoanLockingService; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.exceptions.LockedReadException; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -45,7 +45,7 @@ public class LoanItemListenerStepDefinitions implements En { - private LoanLockingService loanLockingService = mock(LoanLockingService.class); + private LockingService loanLockingService = mock(LockingService.class); private TransactionTemplate transactionTemplate = spy(TransactionTemplate.class); private ChunkProcessingLoanItemListener loanItemListener = new ChunkProcessingLoanItemListener(loanLockingService, transactionTemplate); @@ -58,7 +58,7 @@ public class LoanItemListenerStepDefinitions implements En { public LoanItemListenerStepDefinitions() { Given("/^The LoanItemListener.onReadError method (.*)$/", (String action) -> { ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); - exception = new LoanReadException(1L, new RuntimeException("fail")); + exception = new LockedReadException(1L, new RuntimeException("fail")); loanAccountLock = new LoanAccountLock(1L, LockOwner.LOAN_COB_CHUNK_PROCESSING, LocalDate.now(ZoneId.systemDefault())); when(loanLockingService.findByLoanIdAndLockOwner(1L, LockOwner.LOAN_COB_CHUNK_PROCESSING)).thenReturn(loanAccountLock); transactionTemplate.setTransactionManager(mock(PlatformTransactionManager.class)); @@ -83,7 +83,7 @@ public LoanItemListenerStepDefinitions() { Given("/^The LoanItemListener.onProcessError method (.*)$/", (String action) -> { ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); - exception = new LoanReadException(1L, new RuntimeException("fail")); + exception = new LockedReadException(1L, new RuntimeException("fail")); loanAccountLock = new LoanAccountLock(2L, LockOwner.LOAN_COB_CHUNK_PROCESSING, LocalDate.now(ZoneId.systemDefault())); when(loanLockingService.findByLoanIdAndLockOwner(2L, LockOwner.LOAN_COB_CHUNK_PROCESSING)).thenReturn(loanAccountLock); when(loan.getId()).thenReturn(2L); @@ -108,7 +108,7 @@ public LoanItemListenerStepDefinitions() { Given("/^The LoanItemListener.onWriteError method (.*)$/", (String action) -> { ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); - exception = new LoanReadException(3L, new RuntimeException("fail")); + exception = new LockedReadException(3L, new RuntimeException("fail")); loanAccountLock = new LoanAccountLock(3L, LockOwner.LOAN_COB_CHUNK_PROCESSING, LocalDate.now(ZoneId.systemDefault())); when(loanLockingService.findByLoanIdAndLockOwner(3L, LockOwner.LOAN_COB_CHUNK_PROCESSING)).thenReturn(loanAccountLock); when(loan.getId()).thenReturn(3L); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java index a91f554c6a6..040339fd4f9 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java @@ -35,7 +35,9 @@ import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.exceptions.LoanLockCannotBeAppliedException; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.exceptions.LockCannotBeAppliedException; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; @@ -55,14 +57,14 @@ public class ApplyLoanLockTaskletStepDefinitions implements En { ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(List.class); ArgumentCaptor lockOwnerValueCaptor = ArgumentCaptor.forClass(LockOwner.class); - private LoanLockingService loanLockingService = mock(LoanLockingService.class); + private LockingService loanLockingService = mock(LockingService.class); private FineractProperties fineractProperties = mock(FineractProperties.class); private FineractProperties.FineractQueryProperties fineractQueryProperties = mock(FineractProperties.FineractQueryProperties.class); - private RetrieveLoanIdService retrieveLoanIdService = mock(RetrieveLoanIdService.class); + private RetrieveIdService retrieveIdService = mock(RetrieveIdService.class); private TransactionTemplate transactionTemplate = spy(TransactionTemplate.class); - private ApplyLoanLockTasklet applyLoanLockTasklet = new ApplyLoanLockTasklet(fineractProperties, loanLockingService, - retrieveLoanIdService, transactionTemplate); + private ApplyLoanLockTasklet applyLoanLockTasklet = new ApplyLoanLockTasklet(fineractProperties, loanLockingService, retrieveIdService, + transactionTemplate); private RepeatStatus resultItem; private StepContribution stepContribution; @@ -76,9 +78,8 @@ public ApplyLoanLockTaskletStepDefinitions() { StepExecution stepExecution = new StepExecution("test", jobExecution); ExecutionContext executionContext = new ExecutionContext(); COBParameter loanCOBParameter = new COBParameter(1L, 4L); - executionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER, loanCOBParameter); - lenient().when( - retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) + executionContext.put(LoanCOBConstant.COB_PARAMETER, loanCOBParameter); + lenient().when(retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) .thenReturn(List.of(1L, 2L, 3L, 4L)); stepExecution.setExecutionContext(executionContext); stepContribution = new StepContribution(stepExecution); @@ -142,7 +143,7 @@ public ApplyLoanLockTaskletStepDefinitions() { }); Then("throw LoanLockCannotBeAppliedException exception ApplyLoanLockTasklet.execute method", () -> { - assertThrows(LoanLockCannotBeAppliedException.class, () -> { + assertThrows(LockCannotBeAppliedException.class, () -> { resultItem = applyLoanLockTasklet.execute(stepContribution, null); }); }); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerTest.java index fdd90bb6cc0..a5c1459e91a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerTest.java @@ -27,9 +27,11 @@ import java.util.Map; import java.util.Set; import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.COBConstant; import org.apache.fineract.cob.data.BusinessStepNameAndOrder; import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.data.COBPartition; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.springbatch.PropertyService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -53,7 +55,7 @@ class LoanCOBPartitionerTest { @Mock private COBBusinessStepService cobBusinessStepService; @Mock - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveIdService retrieveIdService; @Mock private JobOperator jobOperator; @Mock @@ -69,13 +71,13 @@ public void testLoanCOBPartitioner() { when(propertyService.getPartitionSize(LoanCOBConstant.JOB_NAME)).thenReturn(5); when(cobBusinessStepService.getCOBBusinessSteps(LoanCOBBusinessStep.class, LoanCOBConstant.LOAN_COB_JOB_NAME)) .thenReturn(BUSINESS_STEP_SET); - when(retrieveLoanIdService.retrieveLoanCOBPartitions(1L, BUSINESS_DATE, false, 5)) + when(retrieveIdService.retrieveLoanCOBPartitions(1L, BUSINESS_DATE, false, 5)) .thenReturn(List.of(new COBPartition(1L,10L, 1L, 5L), new COBPartition(11L,20L, 2L, 4L))); when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getExecutionContext()).thenReturn(executionContext); when(executionContext.get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)).thenReturn(BUSINESS_DATE); when(executionContext.get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME)).thenReturn(false); - LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveLoanIdService, jobOperator,stepExecution, 1L); + LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveIdService, jobOperator,stepExecution, 1L); //when Map partitions = loanCOBPartitioner.partition(1); @@ -95,7 +97,7 @@ public void testLoanCOBPartitionerEmptyBusinessSteps() throws NoSuchJobExecution when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getId()).thenReturn(123L); - LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveLoanIdService, jobOperator, stepExecution, 1L); + LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveIdService, jobOperator, stepExecution, 1L); //when Map partitions = loanCOBPartitioner.partition(1); @@ -111,13 +113,13 @@ public void testLoanCOBPartitionerNoLoansFound() { when(propertyService.getPartitionSize(LoanCOBConstant.JOB_NAME)).thenReturn(5); when(cobBusinessStepService.getCOBBusinessSteps(LoanCOBBusinessStep.class, LoanCOBConstant.LOAN_COB_JOB_NAME)) .thenReturn(BUSINESS_STEP_SET); - when(retrieveLoanIdService.retrieveLoanCOBPartitions(1L, BUSINESS_DATE, false, 5)) + when(retrieveIdService.retrieveLoanCOBPartitions(1L, BUSINESS_DATE, false, 5)) .thenReturn(List.of()); when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getExecutionContext()).thenReturn(executionContext); when(executionContext.get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)).thenReturn(BUSINESS_DATE); when(executionContext.get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME)).thenReturn(false); - LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveLoanIdService, jobOperator,stepExecution, 1L); + LoanCOBPartitioner loanCOBPartitioner = new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveIdService, jobOperator,stepExecution, 1L); //when Map partitions = loanCOBPartitioner.partition(1); @@ -130,13 +132,13 @@ public void testLoanCOBPartitionerNoLoansFound() { private void validatePartitions(Map partitions, int index, long min, long max, String businessDate, String isCatchUp) { Assertions.assertEquals(BUSINESS_STEP_SET, - partitions.get(LoanCOBPartitioner.PARTITION_PREFIX + index).get(LoanCOBConstant.BUSINESS_STEPS)); + partitions.get(COBConstant.PARTITION_PREFIX + index).get(LoanCOBConstant.BUSINESS_STEPS)); Assertions.assertEquals(new COBParameter(min, max), - partitions.get(LoanCOBPartitioner.PARTITION_PREFIX + index).get(LoanCOBConstant.LOAN_COB_PARAMETER)); - Assertions.assertEquals("partition_" + index, partitions.get(LoanCOBPartitioner.PARTITION_PREFIX + index).get("partition")); + partitions.get(COBConstant.PARTITION_PREFIX + index).get(LoanCOBConstant.COB_PARAMETER)); + Assertions.assertEquals("partition_" + index, partitions.get(COBConstant.PARTITION_PREFIX + index).get("partition")); Assertions.assertEquals(businessDate, - partitions.get(LoanCOBPartitioner.PARTITION_PREFIX + index).get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)); + partitions.get(COBConstant.PARTITION_PREFIX + index).get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)); Assertions.assertEquals(isCatchUp, - partitions.get(LoanCOBPartitioner.PARTITION_PREFIX + index).get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME)); + partitions.get(COBConstant.PARTITION_PREFIX + index).get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME)); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java index ac4a858770b..b332495448b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java @@ -39,7 +39,10 @@ import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; -import org.apache.fineract.cob.exceptions.LoanReadException; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.exceptions.LockedReadException; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -53,13 +56,14 @@ public class LoanItemReaderStepDefinitions implements En { private LoanRepository loanRepository = mock(LoanRepository.class); - private RetrieveLoanIdService retrieveLoanIdService = mock(RetrieveLoanIdService.class); + private RetrieveIdService retrieveIdService = mock(RetrieveIdService.class); private CustomJobParameterResolver customJobParameterResolver = mock(CustomJobParameterResolver.class); - private LoanLockingService lockingService = mock(LoanLockingService.class); + private LockingService lockingService = mock(LockingService.class); - private LoanItemReader loanItemReader = new LoanItemReader(loanRepository, retrieveLoanIdService, lockingService); + private LoanItemReader loanItemReader = new LoanItemReader(loanRepository, + new BeforeStepLockingItemReaderHelper(retrieveIdService, lockingService)); private Loan loan = mock(Loan.class); @@ -82,11 +86,11 @@ public LoanItemReaderStepDefinitions() { maxLoanId = splitAccounts.get(splitAccounts.size() - 1); } COBParameter loanCOBParameter = new COBParameter(minLoanId, maxLoanId); - stepExecutionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER, loanCOBParameter); + stepExecutionContext.put(LoanCOBConstant.COB_PARAMETER, loanCOBParameter); stepExecution.setExecutionContext(stepExecutionContext); lenient().when( - this.retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) + this.retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) .thenReturn(splitAccounts); HashMap businessDates = new HashMap<>(); @@ -124,7 +128,7 @@ public LoanItemReaderStepDefinitions() { }); Then("throw exception LoanItemReader.read method", () -> { - assertThrows(LoanReadException.class, () -> { + assertThrows(LockedReadException.class, () -> { resultItem = this.loanItemReader.read(); }); }); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderTest.java index 30dbc970781..33effa8a54e 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderTest.java @@ -36,6 +36,9 @@ import org.apache.fineract.cob.data.COBParameter; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -58,10 +61,10 @@ class LoanItemReaderTest { private LoanRepository loanRepository; @Mock - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveIdService retrieveIdService; @Mock - private LoanLockingService loanLockingService; + private LockingService loanLockingService; @Mock private StepExecution stepExecution; @@ -83,13 +86,14 @@ public void tearDown() { public void testLoanItemReaderSimple() throws Exception { // given ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "test", "test", "UTC", null)); - LoanItemReader loanItemReader = new LoanItemReader(loanRepository, retrieveLoanIdService, loanLockingService); + LoanItemReader loanItemReader = new LoanItemReader(loanRepository, + new BeforeStepLockingItemReaderHelper<>(retrieveIdService, loanLockingService)); when(stepExecution.getExecutionContext()).thenReturn(executionContext); when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getExecutionContext()).thenReturn(executionContext); COBParameter loanCOBParameter = new COBParameter(1L, 5L); - when(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER)).thenReturn(loanCOBParameter); - when(retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) + when(executionContext.get(LoanCOBConstant.COB_PARAMETER)).thenReturn(loanCOBParameter); + when(retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) .thenReturn(new ArrayList<>(List.of(1L, 2L, 3L, 4L, 5L))); List accountLocks = Stream.of(1L, 2L, 3L, 4L, 5L) .map(l -> new LoanAccountLock(l, LockOwner.LOAN_COB_CHUNK_PROCESSING, LocalDate.of(2023, 7, 25))).toList(); @@ -112,13 +116,14 @@ public void testLoanItemReaderSimple() throws Exception { public void testLoanItemReadNoOpenLoansFound() throws Exception { // given ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "test", "test", "UTC", null)); - LoanItemReader loanItemReader = new LoanItemReader(loanRepository, retrieveLoanIdService, loanLockingService); + LoanItemReader loanItemReader = new LoanItemReader(loanRepository, + new BeforeStepLockingItemReaderHelper<>(retrieveIdService, loanLockingService)); when(stepExecution.getExecutionContext()).thenReturn(executionContext); when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getExecutionContext()).thenReturn(executionContext); COBParameter loanCOBParameter = new COBParameter(1L, 5L); - when(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER)).thenReturn(loanCOBParameter); - when(retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) + when(executionContext.get(LoanCOBConstant.COB_PARAMETER)).thenReturn(loanCOBParameter); + when(retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) .thenReturn(new ArrayList<>(List.of())); // when + then @@ -134,13 +139,14 @@ public void testLoanItemReadNoOpenLoansFound() throws Exception { public void testLoanItemReaderMultiThreadRead() throws Exception { // given ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "test", "test", "UTC", null)); - LoanItemReader loanItemReader = new LoanItemReader(loanRepository, retrieveLoanIdService, loanLockingService); + LoanItemReader loanItemReader = new LoanItemReader(loanRepository, + new BeforeStepLockingItemReaderHelper<>(retrieveIdService, loanLockingService)); when(stepExecution.getExecutionContext()).thenReturn(executionContext); when(stepExecution.getJobExecution()).thenReturn(jobExecution); when(jobExecution.getExecutionContext()).thenReturn(executionContext); COBParameter loanCOBParameter = new COBParameter(1L, 100L); - when(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER)).thenReturn(loanCOBParameter); - when(retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) + when(executionContext.get(LoanCOBConstant.COB_PARAMETER)).thenReturn(loanCOBParameter); + when(retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false)) .thenReturn(new ArrayList<>(IntStream.rangeClosed(1, 100).boxed().map(Long::valueOf).toList())); List accountLocks = IntStream.rangeClosed(1, 100).boxed().map(Long::valueOf) .map(l -> new LoanAccountLock(l, LockOwner.LOAN_COB_CHUNK_PROCESSING, LocalDate.of(2023, 7, 25))).toList(); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java index 7264d391c2e..f6f6c568114 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify; import io.cucumber.java8.En; +import org.apache.fineract.cob.domain.LockingService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.mockito.Mockito; @@ -30,7 +31,7 @@ public class LoanItemWriterStepDefinitions implements En { - private final LoanLockingService loanLockingService = mock(LoanLockingService.class); + private final LockingService loanLockingService = mock(LockingService.class); private final LoanRepository loanRepository = mock(LoanRepository.class); private final LoanItemWriter loanItemWriter = new LoanItemWriter(loanLockingService); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImplTest.java index 2d39f1fc9aa..fb3ade8b6e3 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImplTest.java @@ -75,8 +75,7 @@ select min(id) as min, max(id) as max, page, count(id) as count from } private void testRetrieveLoanCOBPartitions(String expectedSQL, boolean isCatchup) { - RetrieveAllNonClosedLoanIdServiceImpl service = new RetrieveAllNonClosedLoanIdServiceImpl(loanRepository, - namedParameterJdbcTemplate); + RetrieveAllNonClosedIdServiceImpl service = new RetrieveAllNonClosedIdServiceImpl(loanRepository, namedParameterJdbcTemplate); LocalDate businessDate = LocalDate.parse("2023-06-28"); service.retrieveLoanCOBPartitions(1L, businessDate, isCatchup, 5); Mockito.verify(namedParameterJdbcTemplate, times(1)).query(sqlCaptor.capture(), paramsCaptor.capture(), rowMapper.capture()); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java index c910415c715..804e9258858 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java @@ -36,7 +36,6 @@ import java.util.List; import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; import org.apache.fineract.cob.exceptions.AccountLockCannotBeOverruledException; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -64,7 +63,7 @@ class InlineLoanCOBExecutorServiceImplTest { @Mock private InlineLoanCOBExecutionDataParser dataParser; @Mock - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveLoanIdService retrieveIdService; @Mock private FineractProperties fineractProperties; @Mock @@ -97,7 +96,7 @@ void shouldExceptionThrownIfLoanIsAlreadyLocked() { when(fineractQueryProperties.getInClauseParameterSizeLimit()).thenReturn(65000); when(fineractApiProperties.getBodyItemSizeLimit()).thenReturn(fineractBodyItemSizeLimitProperties); when(fineractBodyItemSizeLimitProperties.getInlineLoanCob()).thenReturn(1000); - when(retrieveLoanIdService.retrieveLoanIdsBehindDateOrNull(any(), anyList())).thenReturn(List.of(loan)); + when(retrieveIdService.retrieveLoanIdsBehindDateOrNull(any(), anyList())).thenReturn(List.of(loan)); assertThrows(AccountLockCannotBeOverruledException.class, () -> testObj.executeInlineJob(command, "INLINE_LOAN_COB")); } @@ -121,9 +120,9 @@ void shouldListBePartitioned() { when(fineractQueryProperties.getInClauseParameterSizeLimit()).thenReturn(2); when(fineractApiProperties.getBodyItemSizeLimit()).thenReturn(fineractBodyItemSizeLimitProperties); when(fineractBodyItemSizeLimitProperties.getInlineLoanCob()).thenReturn(1000); - when(retrieveLoanIdService.retrieveLoanIdsBehindDateOrNull(any(), anyList())).thenReturn(List.of(loan1, loan2, loan3)); + when(retrieveIdService.retrieveLoanIdsBehindDateOrNull(any(), anyList())).thenReturn(List.of(loan1, loan2, loan3)); assertThrows(AccountLockCannotBeOverruledException.class, () -> testObj.executeInlineJob(command, "INLINE_LOAN_COB")); - verify(retrieveLoanIdService, times(2)).retrieveLoanIdsBehindDateOrNull(any(), anyList()); + verify(retrieveIdService, times(2)).retrieveLoanIdsBehindDateOrNull(any(), anyList()); } @Test diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java index 4bf1f3a807f..93830325576 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java @@ -49,9 +49,9 @@ import java.util.Optional; import java.util.UUID; import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl; import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.cob.service.RetrieveLoanIdService; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; @@ -104,7 +104,7 @@ class LoanCOBApiFilterTest { @Mock private LoanRescheduleRequestRepository loanRescheduleRequestRepository; @Mock - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveLoanIdService retrieveIdService; @BeforeEach public void setUp() { @@ -182,7 +182,7 @@ void shouldProceedWhenUrlDoesNotMatchWithInvalidLoanId() throws ServletException given(context.authenticatedUser()).willReturn(appUser); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.emptyList()); testObj.doFilterInternal(request, response, filterChain); @@ -230,7 +230,7 @@ void shouldProceedWhenLoanIsNotLockedAndNoLoanIsBehind() throws ServletException given(context.authenticatedUser()).willReturn(appUser); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.emptyList()); testObj.doFilterInternal(request, response, filterChain); @@ -260,7 +260,7 @@ void shouldProceedWhenExternalLoanIsNotLockedAndNotBehind() throws ServletExcept given(loanRepository.findIdByExternalId(any())).willReturn(2L); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.emptyList()); testObj.doFilterInternal(request, response, filterChain); @@ -291,7 +291,7 @@ void shouldProceedWhenRescheduleLoanIsNotLockedAndNotBehind() throws ServletExce given(loanRescheduleRequestRepository.getLoanIdByRescheduleRequestId(resourceId)).willReturn(Optional.of(2L)); given(context.authenticatedUser()).willReturn(appUser); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.emptyList()); testObj.doFilterInternal(request, response, filterChain); @@ -323,7 +323,7 @@ void shouldRunInlineCOBAndProceedWhenLoanIsBehind() throws ServletException, IOE given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.singletonList(result)); given(context.authenticatedUser()).willReturn(appUser); @@ -356,7 +356,7 @@ void shouldNotRunInlineCOBAndProceedWhenLoanIsNotBehind() throws ServletExceptio given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); - given(retrieveLoanIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), + given(retrieveIdService.retrieveLoanIdsBehindDate(eq(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)), anyList())).willReturn(Collections.emptyList()); given(context.authenticatedUser()).willReturn(appUser); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelperTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelperTest.java index 9c9a38fe6a2..4c974f52ddb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelperTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelperTest.java @@ -22,9 +22,9 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.List; -import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl; import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.cob.service.RetrieveIdService; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; @@ -56,7 +56,7 @@ public class LoanCOBFilterHelperTest { @Mock private FineractProperties fineractProperties; @Mock - private RetrieveLoanIdService retrieveLoanIdService; + private RetrieveIdService retrieveIdService; @Mock private LoanRescheduleRequestRepository loanRescheduleRequestRepository; diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java index 8c1afd70eea..6b217bcc26a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java @@ -33,7 +33,7 @@ import java.util.List; import java.util.Set; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.cob.service.AccountLockService; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -173,7 +173,7 @@ class LoanAdjustmentServiceImplTest { @Mock private LoanLifecycleStateMachine loanLifecycleStateMachine; @Mock - private LoanAccountLockService loanAccountLockService; + private AccountLockService loanAccountLockService; @Mock private ExternalIdFactory externalIdFactory; @Mock diff --git a/fineract-working-capital-loan/dependencies.gradle b/fineract-working-capital-loan/dependencies.gradle index 1166edec039..639bf2f9ee9 100644 --- a/fineract-working-capital-loan/dependencies.gradle +++ b/fineract-working-capital-loan/dependencies.gradle @@ -20,6 +20,8 @@ dependencies { implementation(project(path: ':fineract-core')) implementation(project(path: ':fineract-loan')) + implementation(project(path: ':fineract-cob')) + implementation(project(path: ':fineract-loan')) implementation('org.apache.avro:avro') implementation( project(path: ':fineract-avro-schemas') @@ -28,6 +30,8 @@ dependencies { implementation( 'org.springframework.boot:spring-boot-starter-web', 'org.springframework.boot:spring-boot-starter-security', + 'org.springframework.batch:spring-batch-integration', + 'org.springframework:spring-context-support', 'jakarta.ws.rs:jakarta.ws.rs-api', 'org.glassfish.jersey.media:jersey-media-multipart', diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/CustomWorkingCapitalLoanAccountLockRepositoryImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/CustomWorkingCapitalLoanAccountLockRepositoryImpl.java new file mode 100644 index 00000000000..400f081faea --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/CustomWorkingCapitalLoanAccountLockRepositoryImpl.java @@ -0,0 +1,56 @@ +/** + * 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.domain; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CustomWorkingCapitalLoanAccountLockRepositoryImpl implements CustomLoanAccountLockRepository { + + @PersistenceContext + private EntityManager entityManager; + + private final DatabaseSpecificSQLGenerator databaseSpecificSQLGenerator; + + @Override + public void updateLoanFromAccountLocks() { + String sql = "UPDATE m_wc_loan SET last_closed_business_date = (select " + + databaseSpecificSQLGenerator.subDate("lck.lock_placed_on_cob_business_date", "1", "DAY") + + """ + from m_wc_loan_account_locks lck + where lck.loan_id = id + and lck.lock_placed_on_cob_business_date is not null + and lck.error is not null + and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING')) + where last_closed_business_date is null and exists (select lck.loan_id + from m_wc_loan_account_locks lck where lck.loan_id = id + and lck.lock_placed_on_cob_business_date is not null and lck.error is not null + and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING')) + """; + + entityManager.createNativeQuery(sql).executeUpdate(); + entityManager.flush(); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalAccountLockRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalAccountLockRepository.java new file mode 100644 index 00000000000..b40f1611885 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalAccountLockRepository.java @@ -0,0 +1,27 @@ +/** + * 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.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface WorkingCapitalAccountLockRepository extends AccountLockRepository, + JpaRepository, JpaSpecificationExecutor {} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalLoanAccountLock.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalLoanAccountLock.java new file mode 100644 index 00000000000..8f6dc97222a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalLoanAccountLock.java @@ -0,0 +1,38 @@ +/** + * 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.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "m_wc_loan_account_locks") +@Getter +@NoArgsConstructor +public class WorkingCapitalLoanAccountLock extends AccountLock { + + private static final long serialVersionUID = -5476985607461625252L; + + public WorkingCapitalLoanAccountLock(Long loanId, LockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) { + super(loanId, lockOwner, lockPlacedOnCobBusinessDate); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/ApplyWorkingCapitalLoanLockTasklet.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/ApplyWorkingCapitalLoanLockTasklet.java new file mode 100644 index 00000000000..8183d5b6254 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/ApplyWorkingCapitalLoanLockTasklet.java @@ -0,0 +1,48 @@ +/** + * 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.workingcapitalloan; + +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.apache.fineract.cob.tasklet.ApplyCommonLockTasklet; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +public class ApplyWorkingCapitalLoanLockTasklet extends ApplyCommonLockTasklet { + + public ApplyWorkingCapitalLoanLockTasklet(FineractProperties fineractProperties, + LockingService loanLockingService, RetrieveIdService retrieveIdService, + TransactionTemplate transactionTemplate) { + super(fineractProperties, loanLockingService, retrieveIdService, transactionTemplate); + } + + @Override + public String getCOBParameter() { + return WorkingCapitalLoanCOBConstant.COB_PARAMETER; + } + + @Override + public LockOwner getLockOwner() { + return LockOwner.LOAN_COB_CHUNK_PROCESSING; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalAccountLockServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalAccountLockServiceImpl.java new file mode 100644 index 00000000000..d10fd7d2009 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalAccountLockServiceImpl.java @@ -0,0 +1,34 @@ +/** + * 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.workingcapitalloan; + +import org.apache.fineract.cob.domain.AccountLockRepository; +import org.apache.fineract.cob.domain.CustomLoanAccountLockRepository; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.cob.service.AbstractAccountLockService; +import org.springframework.stereotype.Service; + +@Service +public class WorkingCapitalAccountLockServiceImpl extends AbstractAccountLockService { + + public WorkingCapitalAccountLockServiceImpl(AccountLockRepository loanAccountLockRepository, + CustomLoanAccountLockRepository customLoanAccountLockRepository) { + super(loanAccountLockRepository, customLoanAccountLockRepository); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBConstant.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBConstant.java new file mode 100644 index 00000000000..96d3e5d8bdb --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBConstant.java @@ -0,0 +1,45 @@ +/** + * 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.workingcapitalloan; + +import lombok.NoArgsConstructor; +import org.apache.fineract.cob.COBConstant; + +@NoArgsConstructor +public final class WorkingCapitalLoanCOBConstant extends COBConstant { + + // Job Related Constants + public static final String WORKING_CAPITAL_JOB_NAME = "WC_LOAN_COB"; + public static final String WORKING_CAPITAL_JOB_HUMAN_READABLE_NAME = "Working Capital Loan COB"; + public static final String WORKING_CAPITAL_LOAN_COB_JOB_NAME = "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS"; + + // Bean Names + public static final String WORKING_CAPITAL_LOAN_COB_STEP = "workingCapitalLoanCOBStep"; + public static final String WORKING_CAPITAL_LOAN_COB_BUSINESS_STEP = "workingCapitalLoanCOBBusinessStep"; + public static final String WORKING_CAPITAL_LOAN_COB_PARTITIONER = "workingCapitalLoanCOBPartitioner"; + + public static final String WORKING_CAPITAL_LOAN_COB_WORKER_STEP = "workingCapitalLoanCOBWorkerStep"; + public static final String WORKING_CAPITAL_LOAN_COB_FLOW = "workingCapitalLoanCOBFlow"; + + public static final String INLINE_WORKING_CAPITAL_LOAN_COB_JOB_NAME = "INLINE_WORKING_CAPITAL_LOAN_COB"; + public static final String WORKING_CAPITAL_LOAN_IDS_PARAMETER_NAME = "LoanIds"; + + public static final String WORKING_CAPITAL_LOAN_COB_PARTITIONER_STEP = "Working Capital Loan COB partition - Step"; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBCustomJobParametersResolverTasklet.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBCustomJobParametersResolverTasklet.java new file mode 100644 index 00000000000..042351ee352 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBCustomJobParametersResolverTasklet.java @@ -0,0 +1,42 @@ +/** + * 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.workingcapitalloan; + +import static org.apache.fineract.cob.COBConstant.BUSINESS_DATE_PARAMETER_NAME; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.common.CustomJobParameterResolver; +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; +import org.springframework.lang.Nullable; + +@RequiredArgsConstructor +public class WorkingCapitalLoanCOBCustomJobParametersResolverTasklet implements Tasklet { + + private final CustomJobParameterResolver customJobParameterResolver; + + @Nullable + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + customJobParameterResolver.resolve(contribution, chunkContext, BUSINESS_DATE_PARAMETER_NAME, BUSINESS_DATE_PARAMETER_NAME); + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBManagerConfiguration.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBManagerConfiguration.java new file mode 100644 index 00000000000..de81d31e7b2 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBManagerConfiguration.java @@ -0,0 +1,108 @@ +/** + * 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.workingcapitalloan; + +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_JOB_HUMAN_READABLE_NAME; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_JOB_NAME; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_PARTITIONER; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_PARTITIONER_STEP; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_STEP; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_WORKER_STEP; +import static org.apache.fineract.infrastructure.jobs.service.JobName.WORKING_CAPITAL_LOAN_COB_JOB; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.common.CustomJobParameterResolver; +import org.apache.fineract.cob.conditions.BatchManagerCondition; +import org.apache.fineract.infrastructure.springbatch.PropertyService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.listener.ExecutionContextPromotionListener; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.integration.config.annotation.EnableBatchIntegration; +import org.springframework.batch.integration.partition.RemotePartitioningManagerStepBuilderFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +@EnableBatchIntegration +@Conditional(BatchManagerCondition.class) +@RequiredArgsConstructor +public class WorkingCapitalLoanCOBManagerConfiguration { + + private final JobRepository jobRepository; + + private final CustomJobParameterResolver customJobParameterResolver; + + private final PlatformTransactionManager transactionManager; + private final RemotePartitioningManagerStepBuilderFactory stepBuilderFactory; + private final COBBusinessStepService cobBusinessStepService; + private final JobOperator jobOperator; + private final DirectChannel inboundRequests; + + private final DirectChannel outboundRequests; + private final PropertyService propertyService; + private final WorkingCapitalLoanRetrieveIdService retrieveIdService; + + @Bean(WORKING_CAPITAL_LOAN_COB_PARTITIONER) + @StepScope + public WorkingCapitalLoanCOBPartitioner workingCapitalLoanCOBPartitioner(@Value("#{stepExecution}") StepExecution stepExecution) { + return new WorkingCapitalLoanCOBPartitioner(jobOperator, stepExecution, WorkingCapitalLoanCOBConstant.NUMBER_OF_DAYS_BEHIND, + retrieveIdService, cobBusinessStepService, propertyService); + } + + @Bean(WORKING_CAPITAL_JOB_HUMAN_READABLE_NAME) + public Job workingCapitalLoanCOBJob(WorkingCapitalLoanCOBPartitioner workingCapitalLoanCOBPartitioner, + ExecutionContextPromotionListener customJobParametersPromotionListener) { + return new JobBuilder(WORKING_CAPITAL_LOAN_COB_JOB.name(), jobRepository) + .start(resolveCustomJobParametersForWorkingCapitalStep(customJobParametersPromotionListener)) + .next(workingCapitalLoanCOBStep(workingCapitalLoanCOBPartitioner)).incrementer(new RunIdIncrementer()) // + .build(); + } + + @Bean + public WorkingCapitalLoanCOBCustomJobParametersResolverTasklet resolveCustomJobParametersForWorkingCapitalTasklet() { + return new WorkingCapitalLoanCOBCustomJobParametersResolverTasklet(customJobParameterResolver); + } + + @Bean + public Step resolveCustomJobParametersForWorkingCapitalStep(ExecutionContextPromotionListener customJobParametersPromotionListener) { + return new StepBuilder("Resolve custom job parameters - Step", jobRepository) + .tasklet(resolveCustomJobParametersForWorkingCapitalTasklet(), transactionManager) + .listener(customJobParametersPromotionListener).build(); + } + + @Bean(WORKING_CAPITAL_LOAN_COB_STEP) + public Step workingCapitalLoanCOBStep(WorkingCapitalLoanCOBPartitioner partitioner) { + return stepBuilderFactory.get(WORKING_CAPITAL_LOAN_COB_PARTITIONER_STEP)// + .partitioner(WORKING_CAPITAL_LOAN_COB_WORKER_STEP, partitioner)// + .pollInterval(propertyService.getPollInterval(WORKING_CAPITAL_JOB_NAME))// + .outputChannel(outboundRequests).build();// + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBPartitioner.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBPartitioner.java new file mode 100644 index 00000000000..ed03b523adf --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBPartitioner.java @@ -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.cob.workingcapitalloan; + +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_JOB_NAME; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_JOB_NAME; + +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.common.CommonPartitioner; +import org.apache.fineract.cob.data.BusinessStepNameAndOrder; +import org.apache.fineract.cob.workingcapitalloan.businessstep.WorkingCapitalLoanCOBBusinessStep; +import org.apache.fineract.infrastructure.springbatch.PropertyService; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.lang.NonNull; + +@Slf4j +public class WorkingCapitalLoanCOBPartitioner extends CommonPartitioner implements Partitioner { + + private final COBBusinessStepService cobBusinessStepService; + private final PropertyService propertyService; + + public WorkingCapitalLoanCOBPartitioner(JobOperator jobOperator, StepExecution stepExecution, Long numberOfDays, + WorkingCapitalLoanRetrieveIdService retrieveIdService, COBBusinessStepService cobBusinessStepService, + PropertyService propertyService) { + super(jobOperator, stepExecution, numberOfDays, retrieveIdService); + this.cobBusinessStepService = cobBusinessStepService; + this.propertyService = propertyService; + } + + @NonNull + @Override + public Map partition(int gridSize) { + int partitionSize = propertyService.getPartitionSize(WORKING_CAPITAL_JOB_NAME); + Set cobBusinessSteps = cobBusinessStepService.getCOBBusinessSteps(WorkingCapitalLoanCOBBusinessStep.class, + WORKING_CAPITAL_LOAN_COB_JOB_NAME); + return getPartitions(partitionSize, cobBusinessSteps); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerConfiguration.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerConfiguration.java new file mode 100644 index 00000000000..407bb948d8e --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerConfiguration.java @@ -0,0 +1,146 @@ +/** + * 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.workingcapitalloan; + +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_JOB_NAME; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_BUSINESS_STEP; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_FLOW; +import static org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_WORKER_STEP; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.conditions.BatchWorkerCondition; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.cob.loan.ContextAwareTaskDecorator; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.infrastructure.springbatch.PropertyService; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.FlowBuilder; +import org.springframework.batch.core.job.flow.Flow; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.SimpleStepBuilder; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.integration.partition.RemotePartitioningWorkerStepBuilderFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.MessageChannel; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +@Configuration +@Conditional(BatchWorkerCondition.class) +@RequiredArgsConstructor +public class WorkingCapitalLoanCOBWorkerConfiguration { + + private final RemotePartitioningWorkerStepBuilderFactory stepBuilderFactory; + private final MessageChannel inboundRequests; + private final JobRepository jobRepository; + private final PropertyService propertyService; + private final PlatformTransactionManager transactionManager; + private final TransactionTemplate transactionTemplate; + private final LockingService wpcLoanLockingService; + private final FineractProperties fineractProperties; + private final WorkingCapitalLoanRetrieveIdService retrieveIdService; + private final WorkingCapitalLoanRepository workingCapitalLoanRepository; + + @Bean(WORKING_CAPITAL_LOAN_COB_FLOW) + public Flow workingCapitalLoanCOBFlow(Step initialisationStep, Step applyWorkingCapitalLockStep, Step workingCapitalLoanCOBBusinessStep, + Step resetContextStep) { + return new FlowBuilder(WORKING_CAPITAL_LOAN_COB_FLOW).start(initialisationStep).next(applyWorkingCapitalLockStep) + .next(workingCapitalLoanCOBBusinessStep).next(resetContextStep).build(); + } + + @Bean(WORKING_CAPITAL_LOAN_COB_WORKER_STEP) + public Step workingCapitalLoanCOBWorkerStep(Flow workingCapitalLoanCOBFlow) { + return stepBuilderFactory.get(WORKING_CAPITAL_LOAN_COB_WORKER_STEP).inputChannel(inboundRequests).flow(workingCapitalLoanCOBFlow) + .build(); + } + + @Bean(WORKING_CAPITAL_LOAN_COB_BUSINESS_STEP) + @StepScope + public Step workingCapitalLoanCOBBusinessStep(@Value("#{stepExecutionContext['partition']}") String partitionName, + TaskExecutor workingCapitalCobTaskExecutor, COBBusinessStepService cobBusinessStepService) { + SimpleStepBuilder stepBuilder = new StepBuilder("Loan Business - Step:" + partitionName, + jobRepository) + .chunk(propertyService.getChunkSize(JobName.LOAN_COB.name()), transactionManager) // + .reader(new WorkingCapitalLoanCOBWorkerItemReader(workingCapitalLoanRepository, + new BeforeStepLockingItemReaderHelper<>(retrieveIdService, wpcLoanLockingService))) // + .processor(new WorkingCapitalLoanCOBWorkerItemProcessor(cobBusinessStepService)) // + .writer(new WorkingCapitalLoanCOBWorkerItemWriter(wpcLoanLockingService, workingCapitalLoanRepository)) // + .faultTolerant() // + .retry(Exception.class) // + .retryLimit(propertyService.getRetryLimit(WORKING_CAPITAL_JOB_NAME)) // + .skip(Exception.class) // + .skipLimit(propertyService.getChunkSize(WORKING_CAPITAL_JOB_NAME) + 1) // + .listener(workingCapitalLoanItemListener()) // + .transactionManager(transactionManager); + + if (propertyService.getThreadPoolMaxPoolSize(WORKING_CAPITAL_JOB_NAME) > 1) { + stepBuilder.taskExecutor(workingCapitalCobTaskExecutor); + } + + return stepBuilder.build(); + } + + @Bean + public TaskExecutor workingCapitalCobTaskExecutor() { + if (propertyService.getThreadPoolMaxPoolSize(WORKING_CAPITAL_JOB_NAME) == 1) { + return new SyncTaskExecutor(); + } + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setThreadNamePrefix("COB-Thread-"); + taskExecutor.setThreadGroupName("COB-Thread"); + taskExecutor.setCorePoolSize(propertyService.getThreadPoolCorePoolSize(WORKING_CAPITAL_JOB_NAME)); + taskExecutor.setMaxPoolSize(propertyService.getThreadPoolMaxPoolSize(WORKING_CAPITAL_JOB_NAME)); + taskExecutor.setQueueCapacity(propertyService.getThreadPoolQueueCapacity(WORKING_CAPITAL_JOB_NAME)); + taskExecutor.setAllowCoreThreadTimeOut(true); + taskExecutor.setTaskDecorator(new ContextAwareTaskDecorator()); + return taskExecutor; + } + + // Lock + @Bean + public WorkingCapitalLoanCOBWorkerItemListener workingCapitalLoanItemListener() { + return new WorkingCapitalLoanCOBWorkerItemListener(wpcLoanLockingService, transactionTemplate); + } + + @Bean("applyWorkingCapitalLockStep") + @StepScope + public Step applyWorkingCapitalLockStep(@Value("#{stepExecutionContext['partition']}") String partitionName) { + return new StepBuilder("Apply lock - Step:" + partitionName, jobRepository) + .tasklet(applyWorkingCapitalLoanLock(), transactionManager).build(); + } + + @Bean + public ApplyWorkingCapitalLoanLockTasklet applyWorkingCapitalLoanLock() { + return new ApplyWorkingCapitalLoanLockTasklet(fineractProperties, wpcLoanLockingService, retrieveIdService, transactionTemplate); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemListener.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemListener.java new file mode 100644 index 00000000000..47923eec591 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemListener.java @@ -0,0 +1,40 @@ +/** + * 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.workingcapitalloan; + +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.cob.listener.AbstractLoanItemListener; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.transaction.support.TransactionTemplate; + +public class WorkingCapitalLoanCOBWorkerItemListener extends AbstractLoanItemListener { + + public WorkingCapitalLoanCOBWorkerItemListener(LockingService lockingService, + TransactionTemplate transactionTemplate) { + super(lockingService, transactionTemplate); + } + + @Override + protected LockOwner getLockOwner() { + return LockOwner.LOAN_COB_CHUNK_PROCESSING; + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemProcessor.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemProcessor.java new file mode 100644 index 00000000000..60e619d586d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemProcessor.java @@ -0,0 +1,45 @@ +/** + * 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.workingcapitalloan; + +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.processor.AbstractItemProcessor; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.annotation.BeforeStep; + +@Slf4j +public class WorkingCapitalLoanCOBWorkerItemProcessor extends AbstractItemProcessor { + + public WorkingCapitalLoanCOBWorkerItemProcessor(COBBusinessStepService cobBusinessStepService) { + super(cobBusinessStepService); + } + + @Override + public void setLastRun(WorkingCapitalLoan processedLoan) { + processedLoan.setLastClosedBusinessDate(getBusinessDate()); + } + + @BeforeStep + public void beforeStep(StepExecution stepExecution) { + setExecutionContext(stepExecution.getExecutionContext()); + setBusinessDate(stepExecution); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemReader.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemReader.java new file mode 100644 index 00000000000..5c291af19ae --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemReader.java @@ -0,0 +1,68 @@ +/** + * 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.workingcapitalloan; + +import java.util.concurrent.LinkedBlockingQueue; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.cob.exceptions.LockedReadException; +import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.annotation.BeforeStep; +import org.springframework.batch.item.ItemReader; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +@Slf4j +@RequiredArgsConstructor +public class WorkingCapitalLoanCOBWorkerItemReader implements ItemReader { + + private final WorkingCapitalLoanRepository repository; + private final BeforeStepLockingItemReaderHelper itemReaderHelper; + + @Setter(AccessLevel.PROTECTED) + private LinkedBlockingQueue remainingData; + + @Nullable + @Override + public WorkingCapitalLoan read() throws Exception { + final Long loanId = remainingData.poll(); + if (loanId != null) { + try { + return repository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); + } catch (Exception e) { + throw new LockedReadException(loanId, e); + } + } + return null; + } + + @BeforeStep + @SuppressWarnings({ "unchecked" }) + public void beforeStep(@NonNull StepExecution stepExecution) { + setRemainingData(itemReaderHelper.filterRemainingData(stepExecution)); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemWriter.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemWriter.java new file mode 100644 index 00000000000..7acc64ea086 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemWriter.java @@ -0,0 +1,57 @@ +/** + * 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.workingcapitalloan; + +import java.util.List; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.data.RepositoryItemWriter; +import org.springframework.data.repository.CrudRepository; + +@Slf4j +public class WorkingCapitalLoanCOBWorkerItemWriter extends RepositoryItemWriter { + + private final LockingService loanLockingService; + + public WorkingCapitalLoanCOBWorkerItemWriter(LockingService loanLockingService, + CrudRepository repository) { + this.loanLockingService = loanLockingService; + setRepository(repository); + } + + @Override + public void write(@NonNull Chunk items) throws Exception { + if (!items.isEmpty()) { + super.write(items); + List loanIds = items.getItems().stream().map(AbstractPersistableCustom::getId).toList(); + loanLockingService.deleteByLoanIdInAndLockOwner(loanIds, getLockOwner()); + } + } + + public LockOwner getLockOwner() { + return LockOwner.LOAN_COB_CHUNK_PROCESSING; + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingConfiguration.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingConfiguration.java new file mode 100644 index 00000000000..fcb51f8959e --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingConfiguration.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.workingcapitalloan; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.conditions.BatchWorkerCondition; +import org.apache.fineract.cob.domain.LockingService; +import org.apache.fineract.cob.domain.WorkingCapitalAccountLockRepository; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +@Conditional(BatchWorkerCondition.class) +@RequiredArgsConstructor +public class WorkingCapitalLoanLockingConfiguration { + + private final JdbcTemplate jdbcTemplate; + private final FineractProperties fineractProperties; + private final WorkingCapitalAccountLockRepository workingCapitalAccountLockRepository; + + @Bean + @ConditionalOnMissingBean + public LockingService workingCapitalLoanLockingService() { + return new WorkingCapitalLoanLockingServiceImpl(jdbcTemplate, fineractProperties, workingCapitalAccountLockRepository); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingServiceImpl.java new file mode 100644 index 00000000000..b208e9a038c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingServiceImpl.java @@ -0,0 +1,53 @@ +/** + * 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.workingcapitalloan; + +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.domain.AbstractLockingService; +import org.apache.fineract.cob.domain.WorkingCapitalAccountLockRepository; +import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.springframework.jdbc.core.JdbcTemplate; + +@Slf4j +public class WorkingCapitalLoanLockingServiceImpl extends AbstractLockingService { + + private static final String BATCH_LOAN_LOCK_INSERT = """ + INSERT INTO m_wc_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date) VALUES (?,?,?,?,?) + """; + + private static final String BATCH_LOAN_LOCK_UPGRADE = """ + UPDATE m_wc_loan_account_locks SET version= version + 1, lock_owner = ?, lock_placed_on = ? WHERE loan_id = ? + """; + + public WorkingCapitalLoanLockingServiceImpl(JdbcTemplate jdbcTemplate, FineractProperties fineractProperties, + WorkingCapitalAccountLockRepository loanAccountLockRepository) { + super(jdbcTemplate, fineractProperties, loanAccountLockRepository); + } + + @Override + protected String getBatchLoanLockUpgrade() { + return BATCH_LOAN_LOCK_UPGRADE; + } + + @Override + protected String getBatchLoanLockInsert() { + return BATCH_LOAN_LOCK_INSERT; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdConfiguration.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdConfiguration.java new file mode 100644 index 00000000000..5391c11174d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdConfiguration.java @@ -0,0 +1,40 @@ +/** + * 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.workingcapitalloan; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +@Configuration +@RequiredArgsConstructor +public class WorkingCapitalLoanRetrieveIdConfiguration { + + private final WorkingCapitalLoanRepository loanRepository; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Bean("workingCapitalLoanRetrieveIdService") + @ConditionalOnMissingBean + public WorkingCapitalLoanRetrieveIdService workingCapitalLoanRetrieveIdService() { + return new WorkingCapitalLoanRetrieveIdServiceImpl(namedParameterJdbcTemplate, loanRepository); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdService.java new file mode 100644 index 00000000000..a237d7b77d7 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdService.java @@ -0,0 +1,23 @@ +/** + * 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.workingcapitalloan; + +import org.apache.fineract.cob.service.RetrieveIdService; + +public interface WorkingCapitalLoanRetrieveIdService extends RetrieveIdService {} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdServiceImpl.java new file mode 100644 index 00000000000..5bea13a0682 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdServiceImpl.java @@ -0,0 +1,113 @@ +/** + * 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.workingcapitalloan; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.fineract.cob.COBConstant; +import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo; +import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; +import org.apache.fineract.cob.data.COBParameter; +import org.apache.fineract.cob.data.COBPartition; +import org.apache.fineract.cob.service.RetrieveIdService; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +@RequiredArgsConstructor +public class WorkingCapitalLoanRetrieveIdServiceImpl implements WorkingCapitalLoanRetrieveIdService { + + private static final Collection NON_CLOSED_LOAN_STATUSES = new ArrayList<>( + Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED, LoanStatus.ACTIVE, + LoanStatus.TRANSFER_IN_PROGRESS, LoanStatus.TRANSFER_ON_HOLD)); + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final WorkingCapitalLoanRepository loanRepository; + + @Override + public List retrieveLoanCOBPartitions(Long numberOfDays, LocalDate businessDate, boolean isCatchUp, int partitionSize) { + StringBuilder sql = new StringBuilder(); + sql.append("select min(id) as min, max(id) as max, page, count(id) as count from "); + sql.append(" (select floor(((row_number() over(order by id))-1) / :pageSize) as page, t.* from "); + sql.append(" (select id from m_wc_loan where loan_status_id in (:statusIds) and "); + if (isCatchUp) { + sql.append("last_closed_business_date = :businessDate "); + } else { + sql.append("(last_closed_business_date = :businessDate or last_closed_business_date is null) "); + } + sql.append("order by id) t) t2 "); + sql.append("group by page "); + sql.append("order by page"); + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("pageSize", partitionSize); + parameters.addValue("statusIds", List.of(100, 200, 300, 303, 304)); + parameters.addValue("businessDate", businessDate.minusDays(numberOfDays)); + return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveIdService::mapRow); + } + + @Override + public List retrieveLoanIdsBehindDate(LocalDate businessDate, List loanIds) { + throw new NotImplementedException(); + } + + @Override + public List retrieveLoanIdsBehindDateOrNull(LocalDate businessDate, List loanIds) { + throw new NotImplementedException(); + } + + @Override + public List retrieveLoanIdsOldestCobProcessed(LocalDate businessDate) { + throw new NotImplementedException(); + } + + @Override + public List retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(COBParameter loanCOBParameter, + boolean isCatchUp) { + if (isCatchUp) { + return loanRepository.findAllLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanIdAndStatuses( + loanCOBParameter.getMinAccountId(), loanCOBParameter.getMaxAccountId(), + ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(COBConstant.NUMBER_OF_DAYS_BEHIND), + NON_CLOSED_LOAN_STATUSES); + } else { + return loanRepository.findAllLoansByLastClosedBusinessDateAndMinAndMaxLoanIdAndStatuses(loanCOBParameter.getMinAccountId(), + loanCOBParameter.getMaxAccountId(), + ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(COBConstant.NUMBER_OF_DAYS_BEHIND), + NON_CLOSED_LOAN_STATUSES); + } + } + + @Override + public List findAllStayedLockedByCobBusinessDate(LocalDate cobBusinessDate) { + throw new NotImplementedException(); + } + + @Override + public List retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List loanIds) { + throw new NotImplementedException(); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DummyBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DummyBusinessStep.java new file mode 100644 index 00000000000..cd051dd48ec --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DummyBusinessStep.java @@ -0,0 +1,46 @@ +/** + * 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.workingcapitalloan.businessstep; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DummyBusinessStep extends WorkingCapitalLoanCOBBusinessStep { + + @Override + public WorkingCapitalLoan execute(WorkingCapitalLoan input) { + log.info("Executing DummyBusinessStep... WorkingCapitalLoan ID = {}", input.getId()); + return input; + } + + @Override + public String getEnumStyledName() { + return "DUMMY_BUSINESS_STEP"; + } + + @Override + public String getHumanReadableName() { + return "Dummy Business Step"; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanCOBBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanCOBBusinessStep.java new file mode 100644 index 00000000000..1899d165e8e --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanCOBBusinessStep.java @@ -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. + */ +package org.apache.fineract.cob.workingcapitalloan.businessstep; + +import org.apache.fineract.cob.COBBusinessStep; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; + +public abstract class WorkingCapitalLoanCOBBusinessStep implements COBBusinessStep {} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java index 8c454b97e2c..4ae794ac19a 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java @@ -18,6 +18,36 @@ */ package org.apache.fineract.portfolio.workingcapitalloanproduct.domain; -public class WorkingCapitalLoan { +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.Version; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatusConverter; + +@Entity +@Table(name = "m_wc_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "wc_loan_account_no_UNIQUE"), + @UniqueConstraint(columnNames = { "external_id" }, name = "wc_loan_externalid_UNIQUE") }) +@Getter +public class WorkingCapitalLoan extends AbstractAuditableWithUTCDateTimeCustom { + + @Version + int version; + + @Setter + @Column(name = "last_closed_business_date") + private LocalDate lastClosedBusinessDate; + + @Setter(AccessLevel.PACKAGE) + @Column(name = "loan_status_id", nullable = false) + @Convert(converter = LoanStatusConverter.class) + private LoanStatus loanStatus; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanRepository.java new file mode 100644 index 00000000000..417ec3ca0f4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanRepository.java @@ -0,0 +1,41 @@ +/** + * 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.workingcapitalloanproduct.repository; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +public interface WorkingCapitalLoanRepository extends JpaRepository, JpaSpecificationExecutor, + CrudRepository { + + @Query("select loan.id from WorkingCapitalLoan loan where loan.id BETWEEN :minAccountId and :maxAccountId and loan.loanStatus in :nonClosedLoanStatuses and :cobBusinessDate = loan.lastClosedBusinessDate") + List findAllLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanIdAndStatuses(Long minAccountId, Long maxAccountId, + LocalDate cobBusinessDate, Collection nonClosedLoanStatuses); + + @Query("select loan.id from WorkingCapitalLoan loan where loan.id BETWEEN :minAccountId and :maxAccountId and loan.loanStatus in :nonClosedLoanStatuses and (:cobBusinessDate = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)") + List findAllLoansByLastClosedBusinessDateAndMinAndMaxLoanIdAndStatuses(Long minAccountId, Long maxAccountId, + LocalDate cobBusinessDate, Collection nonClosedLoanStatuses); +} diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 8a419149422..820301fd22d 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -23,4 +23,6 @@ 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.1.xsd"> + + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0002_wc_loan_schema.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0002_wc_loan_schema.xml new file mode 100644 index 00000000000..a6c104e21cf --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0002_wc_loan_schema.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0003_working_capital_loan_cob.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0003_working_capital_loan_cob.xml new file mode 100644 index 00000000000..e0d7cc18b10 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0003_working_capital_loan_cob.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml index 3ee3dd86491..155b665babc 100644 --- a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml +++ b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml @@ -31,8 +31,11 @@ org.eclipse.persistence.jpa.PersistenceProvider + + + false