diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStep.java new file mode 100644 index 00000000000..44d8629b7d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStep.java @@ -0,0 +1,30 @@ +/** + * 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; + +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +public interface COBBusinessStep { + + T execute(T input); + + String getEnumStyledName(); + + String getHumanReadableName(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java new file mode 100644 index 00000000000..6fed9e74efa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.cob.loan; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ApplyChargeToOverdueLoansBusinessStep implements LoanCOBBusinessStep { + + private final ConfigurationDomainService configurationDomainService; + private final LoanReadPlatformService loanReadPlatformService; + private final LoanWritePlatformService loanWritePlatformService; + + @Override + public Loan execute(Loan input) { + final Long penaltyWaitPeriodValue = configurationDomainService.retrievePenaltyWaitPeriod(); + final Boolean backdatePenalties = configurationDomainService.isBackdatePenaltiesEnabled(); + final Collection overdueLoanScheduledInstallments = loanReadPlatformService + .retrieveAllLoansWithOverdueInstallments(penaltyWaitPeriodValue, backdatePenalties); + // TODO: this is very much not effective to get all overdue installments for each loan, a new method needs to be + // implemented for it + Map> groupedOverdueData = overdueLoanScheduledInstallments.stream() + .collect(Collectors.groupingBy(OverdueLoanScheduleData::getLoanId)); + for (Long loanId : groupedOverdueData.keySet()) { + loanWritePlatformService.applyOverdueChargesForLoan(input.getId(), groupedOverdueData.get(loanId)); + } + return input; + } + + @Override + public String getEnumStyledName() { + return "APPLY_CHARGE_TO_OVERDUE_LOANS"; + } + + @Override + public String getHumanReadableName() { + return "Apply charge to overdue loans"; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBBusinessStep.java new file mode 100644 index 00000000000..5513b5a42ae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBBusinessStep.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.loan; + +import org.apache.fineract.cob.COBBusinessStep; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public interface LoanCOBBusinessStep extends COBBusinessStep {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 9d6583838f1..cf791792fec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -1549,13 +1549,13 @@ public Collection retrieveAllLoansWithOverdueInstallmen .append(" and mc.charge_time_enum = 9 and ml.loan_status_id = 300 "); if (backdatePenalties) { - return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod }); + return this.jdbcTemplate.query(sqlBuilder.toString(), rm, penaltyWaitPeriod); } // Only apply for duedate = yesterday (so that we don't apply // penalties on the duedate itself) sqlBuilder.append(" and ls.duedate >= " + sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "(? + 1)", "day")); - return this.jdbcTemplate.query(sqlBuilder.toString(), rm, new Object[] { penaltyWaitPeriod, penaltyWaitPeriod }); + return this.jdbcTemplate.query(sqlBuilder.toString(), rm, penaltyWaitPeriod, penaltyWaitPeriod); } @SuppressWarnings("deprecation") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSchedularServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSchedularServiceImpl.java index ec173b6440d..3c190c9cd5b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSchedularServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSchedularServiceImpl.java @@ -23,14 +23,18 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.loan.ApplyChargeToOverdueLoansBusinessStep; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; @@ -42,10 +46,8 @@ import org.apache.fineract.organisation.office.data.OfficeData; import org.apache.fineract.organisation.office.exception.OfficeNotFoundException; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.dao.CannotAcquireLockException; import org.springframework.orm.ObjectOptimisticLockingFailureException; @@ -53,27 +55,19 @@ import org.springframework.util.CollectionUtils; @Service +@RequiredArgsConstructor +@Slf4j public class LoanSchedularServiceImpl implements LoanSchedularService { - private static final Logger LOG = LoggerFactory.getLogger(LoanSchedularServiceImpl.class); - private static final SecureRandom random = new SecureRandom(); + private static final SecureRandom RANDOM = new SecureRandom(); private final ConfigurationDomainService configurationDomainService; private final LoanReadPlatformService loanReadPlatformService; private final LoanWritePlatformService loanWritePlatformService; private final OfficeReadPlatformService officeReadPlatformService; private final ApplicationContext applicationContext; - - @Autowired - public LoanSchedularServiceImpl(final ConfigurationDomainService configurationDomainService, - final LoanReadPlatformService loanReadPlatformService, final LoanWritePlatformService loanWritePlatformService, - final OfficeReadPlatformService officeReadPlatformService, final ApplicationContext applicationContext) { - this.configurationDomainService = configurationDomainService; - this.loanReadPlatformService = loanReadPlatformService; - this.loanWritePlatformService = loanWritePlatformService; - this.officeReadPlatformService = officeReadPlatformService; - this.applicationContext = applicationContext; - } + private final ApplyChargeToOverdueLoansBusinessStep applyChargeToOverdueLoansBusinessStep; + private final LoanRepository loanRepository; @Override @CronTarget(jobName = JobName.APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT) @@ -84,36 +78,26 @@ public void applyChargeForOverdueLoans() throws JobExecutionException { final Collection overdueLoanScheduledInstallments = this.loanReadPlatformService .retrieveAllLoansWithOverdueInstallments(penaltyWaitPeriodValue, backdatePenalties); - if (!overdueLoanScheduledInstallments.isEmpty()) { - final Map> overdueScheduleData = new HashMap<>(); - for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduledInstallments) { - if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) { - overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment); - } else { - Collection loanData = new ArrayList<>(); - loanData.add(overdueInstallment); - overdueScheduleData.put(overdueInstallment.getLoanId(), loanData); - } - } + Set loanIds = overdueLoanScheduledInstallments.stream().map(OverdueLoanScheduleData::getLoanId).collect(Collectors.toSet()); + if (!loanIds.isEmpty()) { List exceptions = new ArrayList<>(); - for (final Long loanId : overdueScheduleData.keySet()) { + for (final Long loanId : loanIds) { try { - this.loanWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId)); - + applyChargeToOverdueLoansBusinessStep.execute(loanRepository.getReferenceById(loanId)); } catch (final PlatformApiDataValidationException e) { final List errors = e.getErrors(); for (final ApiParameterError error : errors) { - LOG.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId, + log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId, error.getDeveloperMessage(), e); } exceptions.add(e); } catch (final AbstractPlatformDomainRuleException e) { - LOG.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId, + log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId, e.getDefaultUserMessage(), e); exceptions.add(e); } catch (Exception e) { - LOG.error("Apply Charges due for overdue loans failed for account {}", loanId, e); + log.error("Apply Charges due for overdue loans failed for account {}", loanId, e); exceptions.add(e); } } @@ -135,18 +119,18 @@ public void recalculateInterest() throws JobExecutionException { if (!loanIds.isEmpty()) { List errors = new ArrayList<>(); for (Long loanId : loanIds) { - LOG.info("recalculateInterest: Loan ID = {}", loanId); + log.info("recalculateInterest: Loan ID = {}", loanId); Integer numberOfRetries = 0; while (numberOfRetries <= maxNumberOfRetries) { try { this.loanWritePlatformService.recalculateInterest(loanId); numberOfRetries = maxNumberOfRetries + 1; } catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) { - LOG.info("Recalulate interest job has been retried {} time(s)", numberOfRetries); + log.info("Recalulate interest job has been retried {} time(s)", numberOfRetries); // Fail if the transaction has been retried for // maxNumberOfRetries if (numberOfRetries >= maxNumberOfRetries) { - LOG.error("Recalulate interest job has been retried for the max allowed attempts of {} and will be rolled back", + log.error("Recalulate interest job has been retried for the max allowed attempts of {} and will be rolled back", numberOfRetries); errors.add(exception); break; @@ -154,22 +138,22 @@ public void recalculateInterest() throws JobExecutionException { // Else sleep for a random time (between 1 to 10 // seconds) and continue try { - int randomNum = random.nextInt(maxIntervalBetweenRetries + 1); + int randomNum = RANDOM.nextInt(maxIntervalBetweenRetries + 1); Thread.sleep(1000 + (randomNum * 1000)); numberOfRetries = numberOfRetries + 1; } catch (InterruptedException e) { - LOG.error("Interest recalculation for loans retry failed due to InterruptedException", e); + log.error("Interest recalculation for loans retry failed due to InterruptedException", e); errors.add(e); break; } } catch (Exception e) { - LOG.error("Interest recalculation for loans failed for account {}", loanId, e); + log.error("Interest recalculation for loans failed for account {}", loanId, e); numberOfRetries = maxNumberOfRetries + 1; errors.add(e); } i++; } - LOG.info("recalculateInterest: Loans count {}", i); + log.info("recalculateInterest: Loans count {}", i); } if (!errors.isEmpty()) { throw new JobExecutionException(errors); @@ -183,7 +167,7 @@ public void recalculateInterest() throws JobExecutionException { public void recalculateInterest(Map jobParameters) { // gets the officeId final String officeId = jobParameters.get("officeId"); - LOG.info("recalculateInterest: officeId={}", officeId); + log.info("recalculateInterest: officeId={}", officeId); Long officeIdLong = Long.valueOf(officeId); // gets the Office object @@ -214,7 +198,7 @@ private void recalculateInterest(OfficeData office, int threadPoolSize, int batc // paginated dataset do { int totalFilteredRecords = loanIds.size(); - LOG.info("Starting accrual - total filtered records - {}", totalFilteredRecords); + log.info("Starting accrual - total filtered records - {}", totalFilteredRecords); recalculateInterest(loanIds, threadPoolSize, batchSize, executorService); maxLoanIdInList += pageSize + 1; loanIds = Collections.synchronizedList( @@ -269,7 +253,7 @@ private void recalculateInterest(List loanIds, int threadPoolSize, int bat List> responses = executorService.invokeAll(posters); checkCompletion(responses); } catch (InterruptedException e1) { - LOG.error("Interrupted while recalculateInterest", e1); + log.error("Interrupted while recalculateInterest", e1); } } @@ -301,12 +285,12 @@ private void checkCompletion(List> responses) { } allThreadsExecuted = noOfThreadsExecuted == responses.size(); if (!allThreadsExecuted) { - LOG.error("All threads could not execute."); + log.error("All threads could not execute."); } } catch (InterruptedException e1) { - LOG.error("Interrupted while posting IR entries", e1); + log.error("Interrupted while posting IR entries", e1); } catch (ExecutionException e2) { - LOG.error("Execution exception while posting IR entries", e2); + log.error("Execution exception while posting IR entries", e2); } } }