Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 extends AbstractPersistableCustom> {

T execute(T input);

String getEnumStyledName();

String getHumanReadableName();
}
Original file line number Diff line number Diff line change
@@ -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<OverdueLoanScheduleData> 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<Long, List<OverdueLoanScheduleData>> 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";
}
}
Original file line number Diff line number Diff line change
@@ -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<Loan> {}
Original file line number Diff line number Diff line change
Expand Up @@ -1549,13 +1549,13 @@ public Collection<OverdueLoanScheduleData> 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,38 +46,28 @@
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;
import org.springframework.stereotype.Service;
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)
Expand All @@ -84,36 +78,26 @@ public void applyChargeForOverdueLoans() throws JobExecutionException {
final Collection<OverdueLoanScheduleData> overdueLoanScheduledInstallments = this.loanReadPlatformService
.retrieveAllLoansWithOverdueInstallments(penaltyWaitPeriodValue, backdatePenalties);

if (!overdueLoanScheduledInstallments.isEmpty()) {
final Map<Long, Collection<OverdueLoanScheduleData>> overdueScheduleData = new HashMap<>();
for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduledInstallments) {
if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) {
overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment);
} else {
Collection<OverdueLoanScheduleData> loanData = new ArrayList<>();
loanData.add(overdueInstallment);
overdueScheduleData.put(overdueInstallment.getLoanId(), loanData);
}
}
Set<Long> loanIds = overdueLoanScheduledInstallments.stream().map(OverdueLoanScheduleData::getLoanId).collect(Collectors.toSet());

if (!loanIds.isEmpty()) {
List<Throwable> 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<ApiParameterError> 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);
}
}
Expand All @@ -135,41 +119,41 @@ public void recalculateInterest() throws JobExecutionException {
if (!loanIds.isEmpty()) {
List<Throwable> 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;
}
// 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);
Expand All @@ -183,7 +167,7 @@ public void recalculateInterest() throws JobExecutionException {
public void recalculateInterest(Map<String, String> 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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -269,7 +253,7 @@ private void recalculateInterest(List<Long> loanIds, int threadPoolSize, int bat
List<Future<Void>> responses = executorService.invokeAll(posters);
checkCompletion(responses);
} catch (InterruptedException e1) {
LOG.error("Interrupted while recalculateInterest", e1);
log.error("Interrupted while recalculateInterest", e1);
}
}

Expand Down Expand Up @@ -301,12 +285,12 @@ private void checkCompletion(List<Future<Void>> 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);
}
}
}