| /** |
| * 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.scheduledjobs.service; |
| |
| import java.math.BigDecimal; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.fineract.infrastructure.core.data.ApiParameterError; |
| import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.infrastructure.core.service.RoutingDataSourceServiceFactory; |
| import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; |
| import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; |
| import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; |
| import org.apache.fineract.infrastructure.jobs.service.JobName; |
| import org.apache.fineract.portfolio.savings.DepositAccountType; |
| import org.apache.fineract.portfolio.savings.DepositAccountUtils; |
| import org.apache.fineract.portfolio.savings.data.DepositAccountData; |
| import org.apache.fineract.portfolio.savings.data.SavingsAccountAnnualFeeData; |
| import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; |
| import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; |
| import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; |
| import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; |
| import org.joda.time.LocalDate; |
| import org.joda.time.format.DateTimeFormat; |
| import org.joda.time.format.DateTimeFormatter; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.jdbc.core.JdbcTemplate; |
| import org.springframework.stereotype.Service; |
| import org.springframework.transaction.annotation.Transactional; |
| |
| @Service(value = "scheduledJobRunnerService") |
| public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService { |
| |
| private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class); |
| private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); |
| private final DateTimeFormatter formatterWithTime = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); |
| |
| private final RoutingDataSourceServiceFactory dataSourceServiceFactory; |
| private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; |
| private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; |
| private final DepositAccountReadPlatformService depositAccountReadPlatformService; |
| private final DepositAccountWritePlatformService depositAccountWritePlatformService; |
| |
| @Autowired |
| public ScheduledJobRunnerServiceImpl(final RoutingDataSourceServiceFactory dataSourceServiceFactory, |
| final SavingsAccountWritePlatformService savingsAccountWritePlatformService, |
| final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService, |
| final DepositAccountReadPlatformService depositAccountReadPlatformService, |
| final DepositAccountWritePlatformService depositAccountWritePlatformService) { |
| this.dataSourceServiceFactory = dataSourceServiceFactory; |
| this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; |
| this.savingsAccountChargeReadPlatformService = savingsAccountChargeReadPlatformService; |
| this.depositAccountReadPlatformService = depositAccountReadPlatformService; |
| this.depositAccountWritePlatformService = depositAccountWritePlatformService; |
| } |
| |
| @Transactional |
| @Override |
| @CronTarget(jobName = JobName.UPDATE_LOAN_SUMMARY) |
| public void updateLoanSummaryDetails() { |
| |
| final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); |
| |
| final StringBuilder updateSqlBuilder = new StringBuilder(900); |
| updateSqlBuilder.append("update m_loan "); |
| updateSqlBuilder.append("join ("); |
| updateSqlBuilder.append("SELECT ml.id AS loanId,"); |
| updateSqlBuilder.append("SUM(mr.principal_amount) as principal_disbursed_derived, "); |
| updateSqlBuilder.append("SUM(IFNULL(mr.principal_completed_derived,0)) as principal_repaid_derived, "); |
| updateSqlBuilder.append("SUM(IFNULL(mr.principal_writtenoff_derived,0)) as principal_writtenoff_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.interest_amount,0)) as interest_charged_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.interest_completed_derived,0)) as interest_repaid_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.interest_waived_derived,0)) as interest_waived_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.interest_writtenoff_derived,0)) as interest_writtenoff_derived,"); |
| updateSqlBuilder |
| .append("SUM(IFNULL(mr.fee_charges_amount,0)) + IFNULL((select SUM(lc.amount) from m_loan_charge lc where lc.loan_id=ml.id and lc.is_active=1 and lc.charge_time_enum=1),0) as fee_charges_charged_derived,"); |
| updateSqlBuilder |
| .append("SUM(IFNULL(mr.fee_charges_completed_derived,0)) + IFNULL((select SUM(lc.amount_paid_derived) from m_loan_charge lc where lc.loan_id=ml.id and lc.is_active=1 and lc.charge_time_enum=1),0) as fee_charges_repaid_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_waived_derived,0)) as fee_charges_waived_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_writtenoff_derived,0)) as fee_charges_writtenoff_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.penalty_charges_amount,0)) as penalty_charges_charged_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.penalty_charges_completed_derived,0)) as penalty_charges_repaid_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.penalty_charges_waived_derived,0)) as penalty_charges_waived_derived,"); |
| updateSqlBuilder.append("SUM(IFNULL(mr.penalty_charges_writtenoff_derived,0)) as penalty_charges_writtenoff_derived "); |
| updateSqlBuilder.append(" FROM m_loan ml "); |
| updateSqlBuilder.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); |
| updateSqlBuilder.append("WHERE ml.disbursedon_date is not null "); |
| updateSqlBuilder.append("GROUP BY ml.id "); |
| updateSqlBuilder.append(") x on x.loanId = m_loan.id "); |
| |
| updateSqlBuilder.append("SET m_loan.principal_disbursed_derived = x.principal_disbursed_derived,"); |
| updateSqlBuilder.append("m_loan.principal_repaid_derived = x.principal_repaid_derived,"); |
| updateSqlBuilder.append("m_loan.principal_writtenoff_derived = x.principal_writtenoff_derived,"); |
| updateSqlBuilder |
| .append("m_loan.principal_outstanding_derived = (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)),"); |
| updateSqlBuilder.append("m_loan.interest_charged_derived = x.interest_charged_derived,"); |
| updateSqlBuilder.append("m_loan.interest_repaid_derived = x.interest_repaid_derived,"); |
| updateSqlBuilder.append("m_loan.interest_waived_derived = x.interest_waived_derived,"); |
| updateSqlBuilder.append("m_loan.interest_writtenoff_derived = x.interest_writtenoff_derived,"); |
| updateSqlBuilder |
| .append("m_loan.interest_outstanding_derived = (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)),"); |
| updateSqlBuilder.append("m_loan.fee_charges_charged_derived = x.fee_charges_charged_derived,"); |
| updateSqlBuilder.append("m_loan.fee_charges_repaid_derived = x.fee_charges_repaid_derived,"); |
| updateSqlBuilder.append("m_loan.fee_charges_waived_derived = x.fee_charges_waived_derived,"); |
| updateSqlBuilder.append("m_loan.fee_charges_writtenoff_derived = x.fee_charges_writtenoff_derived,"); |
| updateSqlBuilder |
| .append("m_loan.fee_charges_outstanding_derived = (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)),"); |
| updateSqlBuilder.append("m_loan.penalty_charges_charged_derived = x.penalty_charges_charged_derived,"); |
| updateSqlBuilder.append("m_loan.penalty_charges_repaid_derived = x.penalty_charges_repaid_derived,"); |
| updateSqlBuilder.append("m_loan.penalty_charges_waived_derived = x.penalty_charges_waived_derived,"); |
| updateSqlBuilder.append("m_loan.penalty_charges_writtenoff_derived = x.penalty_charges_writtenoff_derived,"); |
| updateSqlBuilder |
| .append("m_loan.penalty_charges_outstanding_derived = (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived)),"); |
| updateSqlBuilder |
| .append("m_loan.total_expected_repayment_derived = (x.principal_disbursed_derived + x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); |
| updateSqlBuilder |
| .append("m_loan.total_repayment_derived = (x.principal_repaid_derived + x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); |
| updateSqlBuilder |
| .append("m_loan.total_expected_costofloan_derived = (x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); |
| updateSqlBuilder |
| .append("m_loan.total_costofloan_derived = (x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); |
| updateSqlBuilder |
| .append("m_loan.total_waived_derived = (x.interest_waived_derived + x.fee_charges_waived_derived + x.penalty_charges_waived_derived),"); |
| updateSqlBuilder |
| .append("m_loan.total_writtenoff_derived = (x.interest_writtenoff_derived + x.fee_charges_writtenoff_derived + x.penalty_charges_writtenoff_derived),"); |
| updateSqlBuilder.append("m_loan.total_outstanding_derived="); |
| updateSqlBuilder.append(" (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)) + "); |
| updateSqlBuilder |
| .append(" (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)) +"); |
| updateSqlBuilder |
| .append(" (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)) +"); |
| updateSqlBuilder |
| .append(" (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived))"); |
| |
| final int result = jdbcTemplate.update(updateSqlBuilder.toString()); |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); |
| } |
| |
| @Transactional |
| @Override |
| @CronTarget(jobName = JobName.UPDATE_LOAN_PAID_IN_ADVANCE) |
| public void updateLoanPaidInAdvance() { |
| |
| final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); |
| |
| jdbcTemplate.execute("truncate table m_loan_paid_in_advance"); |
| |
| final StringBuilder updateSqlBuilder = new StringBuilder(900); |
| |
| updateSqlBuilder |
| .append("INSERT INTO m_loan_paid_in_advance(loan_id, principal_in_advance_derived, interest_in_advance_derived, fee_charges_in_advance_derived, penalty_charges_in_advance_derived, total_in_advance_derived)"); |
| updateSqlBuilder.append(" select ml.id as loanId,"); |
| updateSqlBuilder.append(" SUM(ifnull(mr.principal_completed_derived, 0)) as principal_in_advance_derived,"); |
| updateSqlBuilder.append(" SUM(ifnull(mr.interest_completed_derived, 0)) as interest_in_advance_derived,"); |
| updateSqlBuilder.append(" SUM(ifnull(mr.fee_charges_completed_derived, 0)) as fee_charges_in_advance_derived,"); |
| updateSqlBuilder.append(" SUM(ifnull(mr.penalty_charges_completed_derived, 0)) as penalty_charges_in_advance_derived,"); |
| updateSqlBuilder |
| .append(" (SUM(ifnull(mr.principal_completed_derived, 0)) + SUM(ifnull(mr.interest_completed_derived, 0)) + SUM(ifnull(mr.fee_charges_completed_derived, 0)) + SUM(ifnull(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); |
| updateSqlBuilder.append(" FROM m_loan ml "); |
| updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); |
| updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); |
| updateSqlBuilder.append(" and mr.duedate >= CURDATE() "); |
| updateSqlBuilder.append(" GROUP BY ml.id"); |
| updateSqlBuilder |
| .append(" HAVING (SUM(ifnull(mr.principal_completed_derived, 0)) + SUM(ifnull(mr.interest_completed_derived, 0)) +"); |
| updateSqlBuilder |
| .append(" SUM(ifnull(mr.fee_charges_completed_derived, 0)) + SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0.0"); |
| |
| final int result = jdbcTemplate.update(updateSqlBuilder.toString()); |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); |
| } |
| |
| @Override |
| @CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS) |
| public void applyAnnualFeeForSavings() { |
| |
| final Collection<SavingsAccountAnnualFeeData> annualFeeData = this.savingsAccountChargeReadPlatformService |
| .retrieveChargesWithAnnualFeeDue(); |
| |
| for (final SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) { |
| try { |
| this.savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(), |
| savingsAccountReference.getAccountId()); |
| } catch (final PlatformApiDataValidationException e) { |
| final List<ApiParameterError> errors = e.getErrors(); |
| for (final ApiParameterError error : errors) { |
| logger.error("Apply annual fee failed for account:" + savingsAccountReference.getAccountNo() + " with message " |
| + error.getDeveloperMessage()); |
| } |
| } catch (final Exception ex) { |
| // need to handle this scenario |
| } |
| } |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + annualFeeData.size()); |
| } |
| |
| @Override |
| @CronTarget(jobName = JobName.PAY_DUE_SAVINGS_CHARGES) |
| public void applyDueChargesForSavings() throws JobExecutionException { |
| final Collection<SavingsAccountAnnualFeeData> chargesDueData = this.savingsAccountChargeReadPlatformService |
| .retrieveChargesWithDue(); |
| final StringBuilder errorMsg = new StringBuilder(); |
| |
| for (final SavingsAccountAnnualFeeData savingsAccountReference : chargesDueData) { |
| try { |
| this.savingsAccountWritePlatformService.applyChargeDue(savingsAccountReference.getId(), |
| savingsAccountReference.getAccountId()); |
| } catch (final PlatformApiDataValidationException e) { |
| final List<ApiParameterError> errors = e.getErrors(); |
| for (final ApiParameterError error : errors) { |
| logger.error("Apply Charges due for savings failed for account:" + savingsAccountReference.getAccountNo() |
| + " with message " + error.getDeveloperMessage()); |
| errorMsg.append("Apply Charges due for savings failed for account:").append(savingsAccountReference.getAccountNo()) |
| .append(" with message ").append(error.getDeveloperMessage()); |
| } |
| } |
| } |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + chargesDueData.size()); |
| |
| /* |
| * throw exception if any charge payment fails. |
| */ |
| if (errorMsg.length() > 0) { throw new JobExecutionException(errorMsg.toString()); } |
| } |
| |
| @Transactional |
| @Override |
| @CronTarget(jobName = JobName.UPDATE_NPA) |
| public void updateNPA() { |
| |
| final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); |
| |
| final StringBuilder resetNPASqlBuilder = new StringBuilder(900); |
| resetNPASqlBuilder.append("update m_loan loan "); |
| resetNPASqlBuilder.append("left join m_loan_arrears_aging laa on laa.loan_id = loan.id "); |
| resetNPASqlBuilder.append("inner join m_product_loan mpl on mpl.id = loan.product_id and mpl.overdue_days_for_npa is not null "); |
| resetNPASqlBuilder.append("set loan.is_npa = 0 "); |
| resetNPASqlBuilder.append("where loan.loan_status_id = 300 and mpl.account_moves_out_of_npa_only_on_arrears_completion = 0 "); |
| resetNPASqlBuilder |
| .append("or (mpl.account_moves_out_of_npa_only_on_arrears_completion = 1 and laa.overdue_since_date_derived is null)"); |
| |
| jdbcTemplate.update(resetNPASqlBuilder.toString()); |
| |
| final StringBuilder updateSqlBuilder = new StringBuilder(900); |
| |
| updateSqlBuilder.append("UPDATE m_loan as ml,"); |
| updateSqlBuilder.append(" (select loan.id "); |
| updateSqlBuilder.append("from m_loan_arrears_aging laa"); |
| updateSqlBuilder.append(" INNER JOIN m_loan loan on laa.loan_id = loan.id "); |
| updateSqlBuilder.append(" INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null "); |
| updateSqlBuilder.append("WHERE loan.loan_status_id = 300 and "); |
| updateSqlBuilder.append("laa.overdue_since_date_derived < SUBDATE(CURDATE(),INTERVAL ifnull(mpl.overdue_days_for_npa,0) day) "); |
| updateSqlBuilder.append("group by loan.id) as sl "); |
| updateSqlBuilder.append("SET ml.is_npa=1 where ml.id=sl.id "); |
| |
| final int result = jdbcTemplate.update(updateSqlBuilder.toString()); |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); |
| } |
| |
| @Override |
| @CronTarget(jobName = JobName.UPDATE_DEPOSITS_ACCOUNT_MATURITY_DETAILS) |
| public void updateMaturityDetailsOfDepositAccounts() { |
| |
| final Collection<DepositAccountData> depositAccounts = this.depositAccountReadPlatformService.retrieveForMaturityUpdate(); |
| |
| for (final DepositAccountData depositAccount : depositAccounts) { |
| try { |
| final DepositAccountType depositAccountType = DepositAccountType.fromInt(depositAccount.depositType().getId().intValue()); |
| this.depositAccountWritePlatformService.updateMaturityDetails(depositAccount.id(), depositAccountType); |
| } catch (final PlatformApiDataValidationException e) { |
| final List<ApiParameterError> errors = e.getErrors(); |
| for (final ApiParameterError error : errors) { |
| logger.error("Update maturity details failed for account:" + depositAccount.accountNo() + " with message " |
| + error.getDeveloperMessage()); |
| } |
| } catch (final Exception ex) { |
| // need to handle this scenario |
| } |
| } |
| |
| logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Deposit accounts affected by update: " + depositAccounts.size()); |
| } |
| |
| @Override |
| @CronTarget(jobName = JobName.GENERATE_RD_SCEHDULE) |
| public void generateRDSchedule() { |
| final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); |
| final Collection<Map<String, Object>> scheduleDetails = this.depositAccountReadPlatformService.retriveDataForRDScheduleCreation(); |
| String insertSql = "INSERT INTO `m_mandatory_savings_schedule` (`savings_account_id`, `duedate`, `installment`, `deposit_amount`, `completed_derived`, `created_date`, `lastmodified_date`) VALUES "; |
| StringBuilder sb = new StringBuilder(); |
| String currentDate = formatterWithTime.print(DateUtils.getLocalDateTimeOfTenant()); |
| int iterations = 0; |
| for (Map<String, Object> details : scheduleDetails) { |
| Long count = (Long) details.get("futureInstallemts"); |
| if (count == null) { |
| count = 0l; |
| } |
| final Long savingsId = (Long) details.get("savingsId"); |
| final BigDecimal amount = (BigDecimal) details.get("amount"); |
| final String recurrence = (String) details.get("recurrence"); |
| Date date = (Date) details.get("dueDate"); |
| LocalDate lastDepositDate = new LocalDate(date); |
| Integer installmentNumber = (Integer) details.get("installment"); |
| while (count < DepositAccountUtils.GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS) { |
| count++; |
| installmentNumber++; |
| lastDepositDate = DepositAccountUtils.calculateNextDepositDate(lastDepositDate, recurrence); |
| |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append("("); |
| sb.append(savingsId); |
| sb.append(",'"); |
| sb.append(formatter.print(lastDepositDate)); |
| sb.append("',"); |
| sb.append(installmentNumber); |
| sb.append(","); |
| sb.append(amount); |
| sb.append(", b'0','"); |
| sb.append(currentDate); |
| sb.append("','"); |
| sb.append(currentDate); |
| sb.append("')"); |
| iterations++; |
| if (iterations > 200) { |
| jdbcTemplate.update(insertSql + sb.toString()); |
| sb = new StringBuilder(); |
| } |
| |
| } |
| } |
| |
| if (sb.length() > 0) { |
| jdbcTemplate.update(insertSql + sb.toString()); |
| } |
| |
| } |
| |
| } |