minor changes to batch jobs code

This commit is contained in:
Vishwas Babu A J
2013-07-13 20:03:29 +05:30
parent bb12ed1c45
commit aa8a6873e0
17 changed files with 354 additions and 293 deletions
@@ -0,0 +1,20 @@
RENAME TABLE `scheduled_jobs` TO `job`;
RENAME TABLE `scheduled_job_runhistory` TO `job_run_history`;
ALTER TABLE `job`
ALTER `job_name` DROP DEFAULT,
ALTER `job_display_name` DROP DEFAULT;
ALTER TABLE `job`
CHANGE COLUMN `job_name` `name` VARCHAR(50) NOT NULL,
CHANGE COLUMN `job_display_name` `display_name` VARCHAR(50) NOT NULL;
ALTER TABLE `job`
CHANGE COLUMN `job_initializing_errorlog` `initializing_errorlog` TEXT NULL;
ALTER TABLE `job_run_history`
ALTER `triggertype` DROP DEFAULT;
ALTER TABLE `job_run_history`
CHANGE COLUMN `errormessage` `error_message` VARCHAR(500) NULL DEFAULT NULL,
CHANGE COLUMN `triggertype` `trigger_type` VARCHAR(25) NOT NULL,
CHANGE COLUMN `errorlog` `error_log` TEXT NULL;
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.annotation; package org.mifosplatform.infrastructure.jobs.annotation;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
@@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@@ -38,11 +39,11 @@ public class CronMethodParser {
public static final int METHOD_INDEX = 1; public static final int METHOD_INDEX = 1;
public static String[] findTargetMethodDetails(String attriuteValue) throws IOException { public static String[] findTargetMethodDetails(String attributeValue) throws IOException {
if (!targetMethosMap.containsKey(attriuteValue)) { if (!targetMethosMap.containsKey(attributeValue)) {
findAnnotationMethods(CronTargetMethod.class, CRON_ANNOTATION_ATTRIBUTE_NAME); findAnnotationMethods(CronTarget.class, CRON_ANNOTATION_ATTRIBUTE_NAME);
} }
return targetMethosMap.get(attriuteValue); return targetMethosMap.get(attributeValue);
} }
/** /**
@@ -61,13 +62,20 @@ public class CronMethodParser {
if (metadataSet != null && metadataSet.size() > 0) { if (metadataSet != null && metadataSet.size() > 0) {
for (MethodMetadata metadata : metadataSet) { for (MethodMetadata metadata : metadataSet) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(annotationClass.getName()); Map<String, Object> attributes = metadata.getAnnotationAttributes(annotationClass.getName());
String attributeValue = (String) attributes.get(attributeName); JobName attributeValue = (JobName) attributes.get(attributeName);
String className = metadata.getDeclaringClassName(); String className = metadata.getDeclaringClassName();
if (attributeValue == null || attributeValue.trim().length() < 1) { /**
attributeValue = className.substring(className.lastIndexOf(".") + 1); * TODO: Vishwas, have commented out this piece of code,
} * check its need with Pramod
**/
/**
* if (attributeValue == null ||
* attributeValue.trim().length() < 1) { attributeValue
* = className.substring(className.lastIndexOf(".") +
* 1); }
**/
String[] mapVal = { className, metadata.getMethodName() }; String[] mapVal = { className, metadata.getMethodName() };
targetMethosMap.put(attributeValue, mapVal); targetMethosMap.put(attributeValue.toString(), mapVal);
} }
} }
} }
@@ -1,10 +1,12 @@
package org.mifosplatform.scheduledjobs.annotation; package org.mifosplatform.infrastructure.jobs.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.mifosplatform.infrastructure.jobs.service.JobName;
/** /**
* Annotation that marks a method to be picked while scheduling a cron jobs. * Annotation that marks a method to be picked while scheduling a cron jobs.
* *
@@ -12,7 +14,7 @@ import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface CronTargetMethod { public @interface CronTarget {
String jobName() default ""; JobName jobName();
} }
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.domain; package org.mifosplatform.infrastructure.jobs.domain;
import java.util.Date; import java.util.Date;
@@ -11,13 +11,13 @@ import javax.persistence.TemporalType;
import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity @Entity
@Table(name = "scheduled_jobs") @Table(name = "job")
public class ScheduledJobDetails extends AbstractPersistable<Long> { public class ScheduledJobDetail extends AbstractPersistable<Long> {
@Column(name = "job_name") @Column(name = "name")
private String jobName; private String jobName;
@Column(name = "job_display_name") @Column(name = "display_name")
private String jobDisplayName; private String jobDisplayName;
@Column(name = "cron_expression") @Column(name = "cron_expression")
@@ -44,17 +44,17 @@ public class ScheduledJobDetails extends AbstractPersistable<Long> {
@Column(name = "trigger_key") @Column(name = "trigger_key")
private String triggerKey; private String triggerKey;
@Column(name = "job_initializing_errorlog") @Column(name = "initializing_errorlog")
private String errorLog; private String errorLog;
@Column(name = "is_active") @Column(name = "is_active")
private boolean activeSchedular; private boolean activeSchedular;
public ScheduledJobDetails() { public ScheduledJobDetail() {
} }
public ScheduledJobDetails(final String jobName, final String jobDisplayName, final String croneExpression, Date createTime, public ScheduledJobDetail(final String jobName, final String jobDisplayName, final String croneExpression, Date createTime,
final Short taskPriority, final String groupName, final Date previousRunStartTime, final Date nextRunTime, final Short taskPriority, final String groupName, final Date previousRunStartTime, final Date nextRunTime,
final String triggKey, final String errorLog, final boolean activeSchedular) { final String triggKey, final String errorLog, final boolean activeSchedular) {
this.jobName = jobName; this.jobName = jobName;
@@ -0,0 +1,14 @@
package org.mifosplatform.infrastructure.jobs.domain;
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.query.Param;
public interface ScheduledJobDetailRepository extends JpaRepository<ScheduledJobDetail, Long>,
JpaSpecificationExecutor<ScheduledJobDetail> {
@Query("from ScheduledJobDetail jobDetail where jobDetail.triggerKey = :triggerKey")
ScheduledJobDetail findByTriggerKey(@Param("triggerKey") String triggerKey);
}
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.domain; package org.mifosplatform.infrastructure.jobs.domain;
import java.util.Date; import java.util.Date;
@@ -13,12 +13,12 @@ import javax.persistence.TemporalType;
import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity @Entity
@Table(name = "scheduled_job_runhistory") @Table(name = "job_run_history")
public class ScheduledJobRunHistory extends AbstractPersistable<Long> { public class ScheduledJobRunHistory extends AbstractPersistable<Long> {
@ManyToOne @ManyToOne
@JoinColumn(name = "job_id") @JoinColumn(name = "job_id")
private ScheduledJobDetails scheduledJobDetails; private ScheduledJobDetail scheduledJobDetail;
@Column(name = "version") @Column(name = "version")
private Long version; private Long version;
@@ -34,22 +34,22 @@ public class ScheduledJobRunHistory extends AbstractPersistable<Long> {
@Column(name = "status") @Column(name = "status")
private String status; private String status;
@Column(name = "errormessage") @Column(name = "error_message")
private String errorMessage; private String errorMessage;
@Column(name = "triggertype") @Column(name = "trigger_type")
private String triggerType; private String triggerType;
@Column(name = "errorlog") @Column(name = "error_log")
private String errorLog; private String errorLog;
public ScheduledJobRunHistory() { public ScheduledJobRunHistory() {
} }
public ScheduledJobRunHistory(final ScheduledJobDetails scheduledJobDetails, final Long version, final Date startTime, public ScheduledJobRunHistory(final ScheduledJobDetail scheduledJobDetail, final Long version, final Date startTime,
final Date endTime, final String status, final String errorMessage, final String triggerType, final String errorLog) { final Date endTime, final String status, final String errorMessage, final String triggerType, final String errorLog) {
this.scheduledJobDetails = scheduledJobDetails; this.scheduledJobDetail = scheduledJobDetail;
this.version = version; this.version = version;
this.startTime = startTime; this.startTime = startTime;
this.endTime = endTime; this.endTime = endTime;
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.domain; package org.mifosplatform.infrastructure.jobs.domain;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -8,7 +8,7 @@ import org.springframework.data.repository.query.Param;
public interface ScheduledJobRunHistoryRepository extends JpaRepository<ScheduledJobRunHistory, Long>, public interface ScheduledJobRunHistoryRepository extends JpaRepository<ScheduledJobRunHistory, Long>,
JpaSpecificationExecutor<ScheduledJobRunHistory> { JpaSpecificationExecutor<ScheduledJobRunHistory> {
@Query("select max(sjrh.version) from ScheduledJobRunHistory sjrh where sjrh.scheduledJobDetails.triggerKey = :triggerKey") @Query("select max(sjrh.version) from ScheduledJobRunHistory sjrh where sjrh.scheduledJobDetail.triggerKey = :triggerKey")
Long findMaxVersionByTriggerKey(@Param("triggerKey") String triggerKey); Long findMaxVersionByTriggerKey(@Param("triggerKey") String triggerKey);
} }
@@ -0,0 +1,19 @@
package org.mifosplatform.infrastructure.jobs.service;
public enum JobName {
UPDATE_LOAN_SUMMARY("Update loan Summary"), UPDATE_LOAN_ARREARS_AGEING("Update Loan Arrears Ageing"), UPDATE_LOAN_PAID_IN_ADVANCE(
"Update Loan Paid In Advance"), APPLY_ANNUAL_FEE_FOR_SAVINGS("Apply Annual Fee For Savings"), APPLY_HOLIDAYS_TO_LOANS(
"Apply Holidays To Loans");
private final String name;
JobName(final String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.service; package org.mifosplatform.infrastructure.jobs.service;
import java.util.List; import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
@@ -7,9 +7,9 @@ import javax.annotation.PostConstruct;
import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.annotation.CronMethodParser;
import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobDetail;
import org.mifosplatform.infrastructure.security.service.TenantDetailsService; import org.mifosplatform.infrastructure.security.service.TenantDetailsService;
import org.mifosplatform.scheduledjobs.annotation.CronMethodParser;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobDetails;
import org.quartz.JobDataMap; import org.quartz.JobDataMap;
import org.quartz.JobDetail; import org.quartz.JobDetail;
import org.quartz.JobKey; import org.quartz.JobKey;
@@ -54,8 +54,8 @@ public class JobRegisterService {
List<MifosPlatformTenant> allTenants = this.tenantDetailsService.findAllTenants(); List<MifosPlatformTenant> allTenants = this.tenantDetailsService.findAllTenants();
for (MifosPlatformTenant tenant : allTenants) { for (MifosPlatformTenant tenant : allTenants) {
ThreadLocalContextUtil.setTenant(tenant); ThreadLocalContextUtil.setTenant(tenant);
List<ScheduledJobDetails> scheduledJobDetails = this.schedularService.getScheduledJobDetails(); List<ScheduledJobDetail> scheduledJobDetails = this.schedularService.getScheduledJobDetails();
for (ScheduledJobDetails jobDetails : scheduledJobDetails) { for (ScheduledJobDetail jobDetails : scheduledJobDetails) {
if (jobDetails.isActiveSchedular()) { if (jobDetails.isActiveSchedular()) {
scheduleJob(jobDetails); scheduleJob(jobDetails);
this.schedularService.saveOrUpdate(jobDetails); this.schedularService.saveOrUpdate(jobDetails);
@@ -76,7 +76,7 @@ public class JobRegisterService {
} }
public void rescheduleJob(ScheduledJobDetails scheduledJobDetails) { public void rescheduleJob(ScheduledJobDetail scheduledJobDetails) {
try { try {
String triggerIdentity = scheduledJobDetails.getTriggerKey(); String triggerIdentity = scheduledJobDetails.getTriggerKey();
String[] triggerKeyIds = triggerIdentity.split(SchedularServiceConstants.TRIGGER_KEY_SEPERATOR); String[] triggerKeyIds = triggerIdentity.split(SchedularServiceConstants.TRIGGER_KEY_SEPERATOR);
@@ -95,7 +95,7 @@ public class JobRegisterService {
} }
} }
private void scheduleJob(ScheduledJobDetails scheduledJobDetails) { private void scheduleJob(ScheduledJobDetail scheduledJobDetails) {
try { try {
JobDetail jobDetail = createJobDetail(scheduledJobDetails); JobDetail jobDetail = createJobDetail(scheduledJobDetails);
Trigger trigger = createTrigger(scheduledJobDetails, jobDetail); Trigger trigger = createTrigger(scheduledJobDetails, jobDetail);
@@ -109,7 +109,7 @@ public class JobRegisterService {
} }
} }
private JobDetail createJobDetail(ScheduledJobDetails scheduledJobDetails) throws Exception { private JobDetail createJobDetail(ScheduledJobDetail scheduledJobDetails) throws Exception {
MifosPlatformTenant tenant = ThreadLocalContextUtil.getTenant(); MifosPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
String[] jobDetails = CronMethodParser.findTargetMethodDetails(scheduledJobDetails.getJobName()); String[] jobDetails = CronMethodParser.findTargetMethodDetails(scheduledJobDetails.getJobName());
String[] beanNames = this.applicationContext.getBeanNamesForType(Class.forName(jobDetails[CronMethodParser.CLASS_INDEX])); String[] beanNames = this.applicationContext.getBeanNamesForType(Class.forName(jobDetails[CronMethodParser.CLASS_INDEX]));
@@ -134,7 +134,7 @@ public class JobRegisterService {
* @param scheduledJobDetails * @param scheduledJobDetails
* @return * @return
*/ */
private Trigger createTrigger(ScheduledJobDetails scheduledJobDetails, JobDetail jobDetail) { private Trigger createTrigger(ScheduledJobDetail scheduledJobDetails, JobDetail jobDetail) {
MifosPlatformTenant tenant = ThreadLocalContextUtil.getTenant(); MifosPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
cronTriggerFactoryBean.setName(scheduledJobDetails.getJobName() + "Trigger-" + tenant.getId()); cronTriggerFactoryBean.setName(scheduledJobDetails.getJobName() + "Trigger-" + tenant.getId());
@@ -0,0 +1,20 @@
package org.mifosplatform.infrastructure.jobs.service;
import java.util.List;
import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobDetail;
import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobRunHistory;
public interface SchedularService {
public List<ScheduledJobDetail> getScheduledJobDetails();
public ScheduledJobDetail getByTriggerKey(String triggerKey);
public void saveOrUpdate(ScheduledJobDetail scheduledJobDetails);
public void saveOrUpdate(ScheduledJobDetail scheduledJobDetails, ScheduledJobRunHistory scheduledJobRunHistory);
public Long getMaxVersionBy(String triggerKey);
}
@@ -1,4 +1,4 @@
package org.mifosplatform.scheduledjobs.service; package org.mifosplatform.infrastructure.jobs.service;
public interface SchedularServiceConstants { public interface SchedularServiceConstants {
@@ -1,11 +1,11 @@
package org.mifosplatform.scheduledjobs.service; package org.mifosplatform.infrastructure.jobs.service;
import java.util.List; import java.util.List;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobDetails; import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobDetail;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobDetailsRepository; import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobDetailRepository;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobRunHistory; import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobRunHistory;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobRunHistoryRepository; import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobRunHistoryRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -13,36 +13,36 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class SchedularServiceJpaRepositoryImpl implements SchedularService { public class SchedularServiceJpaRepositoryImpl implements SchedularService {
private final ScheduledJobDetailsRepository scheduledJobDetailsRepository; private final ScheduledJobDetailRepository scheduledJobDetailsRepository;
private final ScheduledJobRunHistoryRepository scheduledJobRunHistoryRepository; private final ScheduledJobRunHistoryRepository scheduledJobRunHistoryRepository;
@Autowired @Autowired
public SchedularServiceJpaRepositoryImpl(final ScheduledJobDetailsRepository scheduledJobDetailsRepository, public SchedularServiceJpaRepositoryImpl(final ScheduledJobDetailRepository scheduledJobDetailsRepository,
final ScheduledJobRunHistoryRepository scheduledJobRunHistoryRepository) { final ScheduledJobRunHistoryRepository scheduledJobRunHistoryRepository) {
this.scheduledJobDetailsRepository = scheduledJobDetailsRepository; this.scheduledJobDetailsRepository = scheduledJobDetailsRepository;
this.scheduledJobRunHistoryRepository = scheduledJobRunHistoryRepository; this.scheduledJobRunHistoryRepository = scheduledJobRunHistoryRepository;
} }
@Override @Override
public List<ScheduledJobDetails> getScheduledJobDetails() { public List<ScheduledJobDetail> getScheduledJobDetails() {
return scheduledJobDetailsRepository.findAll(); return scheduledJobDetailsRepository.findAll();
} }
@Override @Override
public ScheduledJobDetails getByTriggerKey(final String triggerKey) { public ScheduledJobDetail getByTriggerKey(final String triggerKey) {
return scheduledJobDetailsRepository.findByTriggerKey(triggerKey); return scheduledJobDetailsRepository.findByTriggerKey(triggerKey);
} }
@Transactional @Transactional
@Override @Override
public void saveOrUpdate(final ScheduledJobDetails scheduledJobDetails) { public void saveOrUpdate(final ScheduledJobDetail scheduledJobDetails) {
this.scheduledJobDetailsRepository.save(scheduledJobDetails); this.scheduledJobDetailsRepository.save(scheduledJobDetails);
} }
@Transactional @Transactional
@Override @Override
public void saveOrUpdate(final ScheduledJobDetails scheduledJobDetails, final ScheduledJobRunHistory scheduledJobRunHistory) { public void saveOrUpdate(final ScheduledJobDetail scheduledJobDetails, final ScheduledJobRunHistory scheduledJobRunHistory) {
this.scheduledJobDetailsRepository.save(scheduledJobDetails); this.scheduledJobDetailsRepository.save(scheduledJobDetails);
this.scheduledJobRunHistoryRepository.save(scheduledJobRunHistory); this.scheduledJobRunHistoryRepository.save(scheduledJobRunHistory);
} }
@@ -1,12 +1,12 @@
package org.mifosplatform.scheduledjobs.service; package org.mifosplatform.infrastructure.jobs.service;
import java.util.Date; import java.util.Date;
import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobDetail;
import org.mifosplatform.infrastructure.jobs.domain.ScheduledJobRunHistory;
import org.mifosplatform.infrastructure.security.service.TenantDetailsService; import org.mifosplatform.infrastructure.security.service.TenantDetailsService;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobDetails;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobRunHistory;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException; import org.quartz.JobExecutionException;
import org.quartz.JobListener; import org.quartz.JobListener;
@@ -60,7 +60,7 @@ public class SchedulerJobListener implements JobListener {
Trigger trigger = context.getTrigger(); Trigger trigger = context.getTrigger();
TriggerKey key = trigger.getKey(); TriggerKey key = trigger.getKey();
String triggerKey = key.getName() + SchedularServiceConstants.TRIGGER_KEY_SEPERATOR + key.getGroup(); String triggerKey = key.getName() + SchedularServiceConstants.TRIGGER_KEY_SEPERATOR + key.getGroup();
final ScheduledJobDetails scheduledJobDetails = schedularService.getByTriggerKey(triggerKey); final ScheduledJobDetail scheduledJobDetails = schedularService.getByTriggerKey(triggerKey);
Long version = schedularService.getMaxVersionBy(triggerKey) + 1; Long version = schedularService.getMaxVersionBy(triggerKey) + 1;
String status = SchedularServiceConstants.STATUS_SUCCESS; String status = SchedularServiceConstants.STATUS_SUCCESS;
String errorMessage = null; String errorMessage = null;
@@ -21,6 +21,8 @@ import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomain
import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.domain.HolidayRepository; import org.mifosplatform.organisation.holiday.domain.HolidayRepository;
@@ -75,7 +77,6 @@ import org.mifosplatform.portfolio.note.domain.Note;
import org.mifosplatform.portfolio.note.domain.NoteRepository; import org.mifosplatform.portfolio.note.domain.NoteRepository;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
import org.mifosplatform.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.mifosplatform.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.mifosplatform.scheduledjobs.annotation.CronTargetMethod;
import org.mifosplatform.useradministration.domain.AppUser; import org.mifosplatform.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -113,8 +114,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper, final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository,
final PaymentDetailWritePlatformService paymentDetailWritePlatformService, final PaymentDetailWritePlatformService paymentDetailWritePlatformService, final HolidayRepository holidayRepository,
final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService) { final ConfigurationDomainService configurationDomainService) {
this.context = context; this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanAssembler = loanAssembler; this.loanAssembler = loanAssembler;
@@ -149,15 +150,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanEventApiJsonValidator.validateDisbursement(command.json()); this.loanEventApiJsonValidator.validateDisbursement(command.json());
final Loan loan = retrieveLoanBy(loanId); final Loan loan = retrieveLoanBy(loanId);
// validate actual disbursement date against meeting date // validate actual disbursement date against meeting date
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
CalendarEntityType.LOANS.getValue()); CalendarEntityType.LOANS.getValue());
if(loan.isSyncDisbursementWithMeeting()){ if (loan.isSyncDisbursementWithMeeting()) {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance); this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance);
} }
final MonetaryCurrency currency = loan.getCurrency(); final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
@@ -168,12 +169,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
//Recalculate first repayment date based in actual disbursement date. // Recalculate first repayment date based in actual disbursement date.
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance); final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), actualDisbursementDate.toDate()); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(),
actualDisbursementDate.toDate());
updateLoanCounters(loan, actualDisbursementDate); updateLoanCounters(loan, actualDisbursementDate);
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
@@ -202,39 +204,33 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
private LocalDate getFirstRepaymentMeetingDate( private LocalDate getFirstRepaymentMeetingDate(final LocalDate actualDisbursementDate, final Loan loan,
final LocalDate actualDisbursementDate, final Loan loan, final CalendarInstance calendarInstance) {
final CalendarInstance calendarInstance) { final Calendar calendar = (calendarInstance == null) ? null : calendarInstance.getCalendar();
final Calendar calendar = (calendarInstance == null) ? null LocalDate firstRepaymentMeetingDate = null;
: calendarInstance.getCalendar(); if (calendar != null) {// sync repayments
LocalDate firstRepaymentMeetingDate = null;
if (calendar != null) {// sync repayments // TODO: AA - user provided first repayment date takes precedence
// over recalculated meeting date
if (loan.getExpectedFirstRepaymentOnDate() == null) {
// FIXME: AA - Possibility of having next meeting date
// immediately after disbursement date,
// need to have minimum number of days gap between disbursement
// and first repayment date.
final LoanProductRelatedDetail repaymentScheduleDetails = loan.repaymentScheduleDetail();
if (repaymentScheduleDetails != null) {// Not expecting to be
// null
final Integer repayEvery = repaymentScheduleDetails.getRepayEvery();
final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails
.getRepaymentPeriodFrequencyType());
firstRepaymentMeetingDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, actualDisbursementDate, repayEvery,
frequency);
}
}
}
return firstRepaymentMeetingDate;
}
// TODO: AA - user provided first repayment date takes precedence over recalculated meeting date
if (loan.getExpectedFirstRepaymentOnDate() == null) {
// FIXME: AA - Possibility of having next meeting date
// immediately after disbursement date,
// need to have minimum number of days gap between disbursement
// and first repayment date.
final LoanProductRelatedDetail repaymentScheduleDetails = loan
.repaymentScheduleDetail();
if (repaymentScheduleDetails != null) {// Not expecting to be
// null
final Integer repayEvery = repaymentScheduleDetails
.getRepayEvery();
final String frequency = CalendarHelper
.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails
.getRepaymentPeriodFrequencyType());
firstRepaymentMeetingDate = CalendarHelper
.getFirstRepaymentMeetingDate(calendar,
actualDisbursementDate, repayEvery,
frequency);
}
}
}
return firstRepaymentMeetingDate;
}
/**** /****
* TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base
*****/ *****/
@@ -259,9 +255,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
// Bulk disbursement should happen on meeting date (mostly from collection sheet). // Bulk disbursement should happen on meeting date (mostly from
//FIXME: AA - this should be first meeting date based on disbursement date and next available meeting dates // collection sheet).
//assuming repayment schedule won't regenerate because expected disbursement and actual disbursement happens on same date // FIXME: AA - this should be first meeting date based on
// disbursement date and next available meeting dates
// assuming repayment schedule won't regenerate because expected
// disbursement and actual disbursement happens on same date
final LocalDate firstRepaymentOnDate = null; final LocalDate firstRepaymentOnDate = null;
final boolean isHolidayEnabled = false; final boolean isHolidayEnabled = false;
final List<Holiday> holidays = null; final List<Holiday> holidays = null;
@@ -358,15 +357,20 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private CommandProcessingResultBuilder saveLoanRepayment(final Long loanId, final PaymentDetail paymentDetail, private CommandProcessingResultBuilder saveLoanRepayment(final Long loanId, final PaymentDetail paymentDetail,
final BigDecimal transactionAmount, final LocalDate transactionDate, final String noteText) { final BigDecimal transactionAmount, final LocalDate transactionDate, final String noteText) {
final Loan loan = retrieveLoanBy(loanId); final Loan loan = retrieveLoanBy(loanId);
//TODO: Is it required to validate transaction date with meeting dates if repayments is synced with meeting? // TODO: Is it required to validate transaction date with meeting dates
/*if(loan.isSyncDisbursementWithMeeting()){ // if repayments is synced with meeting?
// validate actual disbursement date against meeting date /*
CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), * if(loan.isSyncDisbursementWithMeeting()){ // validate actual
CalendarEntityType.LOANS.getValue()); * disbursement date against meeting date CalendarInstance
this.loanEventApiJsonValidator.validateRepaymentDateWithMeetingDate(transactionDate, calendarInstance); * calendarInstance =
}*/ * this.calendarInstanceRepository.findCalendarInstaneByLoanId
* (loan.getId(), CalendarEntityType.LOANS.getValue());
* this.loanEventApiJsonValidator
* .validateRepaymentDateWithMeetingDate(transactionDate,
* calendarInstance); }
*/
final List<Long> existingTransactionIds = new ArrayList<Long>(); final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>(); final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
@@ -1008,29 +1012,30 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional @Transactional
@Override @Override
public void updateRepaymentsSchedule(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) { public void updateRepaymentsSchedule(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final Collection<Integer> loanStatuses = new ArrayList<Integer>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), final Collection<Integer> loanStatuses = new ArrayList<Integer>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue())); LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
final Collection<Integer> loanTypes = new ArrayList<Integer>(Arrays.asList(LoanType.GROUP.getValue(), LoanType.JLG.getValue())); final Collection<Integer> loanTypes = new ArrayList<Integer>(Arrays.asList(LoanType.GROUP.getValue(), LoanType.JLG.getValue()));
final Collection<Long> loanIds = new ArrayList<Long>(loanCalendarInstances.size()); final Collection<Long> loanIds = new ArrayList<Long>(loanCalendarInstances.size());
//loop through loanCalendarInstances to get loan ids // loop through loanCalendarInstances to get loan ids
for (CalendarInstance calendarInstance : loanCalendarInstances) { for (CalendarInstance calendarInstance : loanCalendarInstances) {
loanIds.add(calendarInstance.getEntityId()); loanIds.add(calendarInstance.getEntityId());
} }
final List<Loan> loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes); final List<Loan> loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes);
List<Holiday> holidays = null; List<Holiday> holidays = null;
//loop through each loan to reschedule the repayment dates // loop through each loan to reschedule the repayment dates
for (Loan loan : loans) { for (Loan loan : loans) {
if(loan != null){ if (loan != null) {
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate()); holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, holidays); loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
holidays);
this.loanRepository.save(loan); this.loanRepository.save(loan);
} }
} }
} }
private void removeLoanCycle(final Loan loan) { private void removeLoanCycle(final Loan loan) {
final List<Loan> loansToUpdate; final List<Loan> loansToUpdate;
if (loan.isGroupLoan()) { if (loan.isGroupLoan()) {
@@ -1059,7 +1064,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.updateLoanProductLoanCounter(null); loan.updateLoanProductLoanCounter(null);
} }
private void updateLoanCounters(final Loan loan, final LocalDate actualDisbursementDate) { private void updateLoanCounters(final Loan loan, final LocalDate actualDisbursementDate) {
if (loan.isGroupLoan()) { if (loan.isGroupLoan()) {
@@ -1105,7 +1110,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final boolean includeInBorrowerCycle = loan.loanProduct().isIncludeInBorrowerCycle(); final boolean includeInBorrowerCycle = loan.loanProduct().isIncludeInBorrowerCycle();
for (final Loan loanToUpdate : loansToUpdateForLoanCounter) { for (final Loan loanToUpdate : loansToUpdateForLoanCounter) {
//Update client loan counter if loan product includeInBorrowerCycle is true // Update client loan counter if loan product includeInBorrowerCycle
// is true
if (includeInBorrowerCycle) { if (includeInBorrowerCycle) {
Integer currentLoanCounter = loanToUpdate.getCurrentLoanCounter(); Integer currentLoanCounter = loanToUpdate.getCurrentLoanCounter();
if (newLoanCounter > currentLoanCounter) { if (newLoanCounter > currentLoanCounter) {
@@ -1154,7 +1160,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
return maxLoanProductLoanCounter; return maxLoanProductLoanCounter;
} }
private void updateLoanCycleCounter(final List<Loan> loansToUpdate, final Loan loan) { private void updateLoanCycleCounter(final List<Loan> loansToUpdate, final Loan loan) {
final Integer currentLoancounter = loan.getCurrentLoanCounter(); final Integer currentLoancounter = loan.getCurrentLoanCounter();
@@ -1176,11 +1182,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
this.loanRepository.save(loansToUpdate); this.loanRepository.save(loansToUpdate);
} }
@Transactional @Transactional
@Override @Override
@CronTargetMethod(jobName="Apply Holidays To Loans") @CronTarget(jobName = JobName.APPLY_HOLIDAYS_TO_LOANS)
public void applyHolidaysToLoans() { public void applyHolidaysToLoans() {
final Collection<Integer> loanStatuses = new ArrayList<Integer>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), final Collection<Integer> loanStatuses = new ArrayList<Integer>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue())); LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
@@ -1198,10 +1204,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// get all loans // get all loans
List<Loan> loans = new ArrayList<Loan>(); List<Loan> loans = new ArrayList<Loan>();
//get all individual and jlg loans // get all individual and jlg loans
loans.addAll(this.loanRepository.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses)); loans.addAll(this.loanRepository.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses));
//FIXME: optimize to get all client and group loans belongs to a office id // FIXME: optimize to get all client and group loans belongs to a
//get all group loans // office id
// get all group loans
loans.addAll(this.loanRepository.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses)); loans.addAll(this.loanRepository.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (Loan loan : loans) { for (Loan loan : loans) {
@@ -1210,7 +1217,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
this.loanRepository.save(loans); this.loanRepository.save(loans);
holiday.processed(); holiday.processed();
} }
this.holidayRepository.save(holidays); this.holidayRepository.save(holidays);
} }
} }
@@ -1,14 +0,0 @@
package org.mifosplatform.scheduledjobs.domain;
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.query.Param;
public interface ScheduledJobDetailsRepository extends JpaRepository<ScheduledJobDetails, Long>,
JpaSpecificationExecutor<ScheduledJobDetails> {
@Query("from ScheduledJobDetails jobDetails where jobDetails.triggerKey = :triggerKey")
ScheduledJobDetails findByTriggerKey(@Param("triggerKey") String triggerKey);
}
@@ -1,20 +0,0 @@
package org.mifosplatform.scheduledjobs.service;
import java.util.List;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobDetails;
import org.mifosplatform.scheduledjobs.domain.ScheduledJobRunHistory;
public interface SchedularService {
public List<ScheduledJobDetails> getScheduledJobDetails();
public ScheduledJobDetails getByTriggerKey(String triggerKey);
public void saveOrUpdate(ScheduledJobDetails scheduledJobDetails);
public void saveOrUpdate(ScheduledJobDetails scheduledJobDetails, ScheduledJobRunHistory scheduledJobRunHistory);
public Long getMaxVersionBy(String triggerKey);
}
@@ -9,8 +9,9 @@ import java.util.List;
import org.mifosplatform.infrastructure.core.service.RoutingDataSourceServiceFactory; import org.mifosplatform.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.mifosplatform.scheduledjobs.annotation.CronTargetMethod;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -34,171 +35,175 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
@Transactional @Transactional
@Override @Override
@CronTargetMethod(jobName="Update loan Summary") @CronTarget(jobName = JobName.UPDATE_LOAN_SUMMARY)
public void updateLoanSummaryDetails() { public void updateLoanSummaryDetails() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
StringBuilder updateSqlBuilder = new StringBuilder(900); StringBuilder updateSqlBuilder = new StringBuilder(900);
updateSqlBuilder.append("update m_loan "); updateSqlBuilder.append("update m_loan ");
updateSqlBuilder.append("join ("); updateSqlBuilder.append("join (");
updateSqlBuilder.append("SELECT ml.id AS loanId,"); updateSqlBuilder.append("SELECT ml.id AS loanId,");
updateSqlBuilder.append("SUM(mr.principal_amount) as principal_disbursed_derived, "); 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_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.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_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_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_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.interest_writtenoff_derived,0)) as interest_writtenoff_derived,");
updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_amount,0)) as fee_charges_charged_derived,"); updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_amount,0)) as fee_charges_charged_derived,");
updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_completed_derived,0)) as fee_charges_repaid_derived,"); updateSqlBuilder.append("SUM(IFNULL(mr.fee_charges_completed_derived,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_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.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_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_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_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("SUM(IFNULL(mr.penalty_charges_writtenoff_derived,0)) as penalty_charges_writtenoff_derived ");
updateSqlBuilder.append(" FROM m_loan ml "); updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); 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("WHERE ml.disbursedon_date is not null ");
updateSqlBuilder.append("GROUP BY ml.id "); updateSqlBuilder.append("GROUP BY ml.id ");
updateSqlBuilder.append(") x on x.loanId = m_loan.id "); updateSqlBuilder.append(") x on x.loanId = m_loan.id ");
updateSqlBuilder.append("SET m_loan.principal_disbursed_derived = x.principal_disbursed_derived,"); 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_repaid_derived = x.principal_repaid_derived,");
updateSqlBuilder.append("m_loan.principal_writtenoff_derived = x.principal_writtenoff_derived,"); updateSqlBuilder.append("m_loan.principal_writtenoff_derived = x.principal_writtenoff_derived,");
updateSqlBuilder updateSqlBuilder
.append("m_loan.principal_outstanding_derived = (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)),"); .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_charged_derived = x.interest_charged_derived,");
updateSqlBuilder.append("m_loan.interest_repaid_derived = x.interest_repaid_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_waived_derived = x.interest_waived_derived,");
updateSqlBuilder.append("m_loan.interest_writtenoff_derived = x.interest_writtenoff_derived,"); updateSqlBuilder.append("m_loan.interest_writtenoff_derived = x.interest_writtenoff_derived,");
updateSqlBuilder updateSqlBuilder
.append("m_loan.interest_outstanding_derived = (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)),"); .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_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_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_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_writtenoff_derived = x.fee_charges_writtenoff_derived,");
updateSqlBuilder 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)),"); .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_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_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_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_writtenoff_derived = x.penalty_charges_writtenoff_derived,");
updateSqlBuilder 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)),"); .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 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),"); .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 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),"); .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 updateSqlBuilder
.append("m_loan.total_expected_costofloan_derived = (x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); .append("m_loan.total_expected_costofloan_derived = (x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),");
updateSqlBuilder updateSqlBuilder
.append("m_loan.total_costofloan_derived = (x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); .append("m_loan.total_costofloan_derived = (x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),");
updateSqlBuilder updateSqlBuilder
.append("m_loan.total_waived_derived = (x.interest_waived_derived + x.fee_charges_waived_derived + x.penalty_charges_waived_derived),"); .append("m_loan.total_waived_derived = (x.interest_waived_derived + x.fee_charges_waived_derived + x.penalty_charges_waived_derived),");
updateSqlBuilder updateSqlBuilder
.append("m_loan.total_writtenoff_derived = (x.interest_writtenoff_derived + x.fee_charges_writtenoff_derived + x.penalty_charges_writtenoff_derived),"); .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("m_loan.total_outstanding_derived=");
updateSqlBuilder.append(" (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)) + "); updateSqlBuilder.append(" (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)) + ");
updateSqlBuilder updateSqlBuilder
.append(" (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)) +"); .append(" (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)) +");
updateSqlBuilder updateSqlBuilder
.append(" (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)) +"); .append(" (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)) +");
updateSqlBuilder updateSqlBuilder
.append(" (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived))"); .append(" (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived))");
int result = jdbcTemplate.update(updateSqlBuilder.toString()); int result = jdbcTemplate.update(updateSqlBuilder.toString());
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
} }
@Transactional @Transactional
@Override @Override
@CronTargetMethod(jobName="Update Loan Arrears Ageing") @CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING)
public void updateLoanArrearsAgeingDetails() { public void updateLoanArrearsAgeingDetails() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
jdbcTemplate.execute("truncate table m_loan_arrears_aging"); jdbcTemplate.execute("truncate table m_loan_arrears_aging");
StringBuilder updateSqlBuilder = new StringBuilder(900); StringBuilder updateSqlBuilder = new StringBuilder(900);
updateSqlBuilder updateSqlBuilder
.append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)"); .append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)");
updateSqlBuilder.append("select ml.id as loanId,"); updateSqlBuilder.append("select ml.id as loanId,");
updateSqlBuilder updateSqlBuilder
.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,"); .append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,");
updateSqlBuilder updateSqlBuilder
.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,"); .append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,");
updateSqlBuilder updateSqlBuilder
.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,"); .append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,");
updateSqlBuilder updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,"); .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,");
updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +"); updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +"); updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +"); updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +");
updateSqlBuilder updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,"); .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,");
updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived "); updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived ");
updateSqlBuilder.append(" FROM m_loan ml "); updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active
updateSqlBuilder.append(" and mr.completed_derived is false "); updateSqlBuilder.append(" and mr.completed_derived is false ");
updateSqlBuilder.append(" and mr.duedate < CURDATE() "); updateSqlBuilder.append(" and mr.duedate < CURDATE() ");
updateSqlBuilder.append(" GROUP BY ml.id"); updateSqlBuilder.append(" GROUP BY ml.id");
int result = jdbcTemplate.update(updateSqlBuilder.toString()); int result = jdbcTemplate.update(updateSqlBuilder.toString());
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
} }
@Transactional @Transactional
@Override @Override
@CronTargetMethod(jobName="Update Loan Paid In Advance") @CronTarget(jobName = JobName.UPDATE_LOAN_PAID_IN_ADVANCE)
public void updateLoanPaidInAdvance() { public void updateLoanPaidInAdvance() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
jdbcTemplate.execute("truncate table m_loan_paid_in_advance"); jdbcTemplate.execute("truncate table m_loan_paid_in_advance");
StringBuilder updateSqlBuilder = new StringBuilder(900); 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
updateSqlBuilder.append(" select ml.id as loanId,"); .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(" SUM(ifnull(mr.principal_completed_derived, 0)) as principal_in_advance_derived,"); updateSqlBuilder.append(" select ml.id as loanId,");
updateSqlBuilder.append(" SUM(ifnull(mr.interest_completed_derived, 0)) as interest_in_advance_derived,"); updateSqlBuilder.append(" SUM(ifnull(mr.principal_completed_derived, 0)) as principal_in_advance_derived,");
updateSqlBuilder.append(" SUM(ifnull(mr.fee_charges_completed_derived, 0)) as fee_charges_in_advance_derived,"); updateSqlBuilder.append(" SUM(ifnull(mr.interest_completed_derived, 0)) as interest_in_advance_derived,");
updateSqlBuilder.append(" SUM(ifnull(mr.penalty_charges_completed_derived, 0)) as penalty_charges_in_advance_derived,"); updateSqlBuilder.append(" SUM(ifnull(mr.fee_charges_completed_derived, 0)) as fee_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(" SUM(ifnull(mr.penalty_charges_completed_derived, 0)) as penalty_charges_in_advance_derived,");
updateSqlBuilder.append(" FROM m_loan ml "); updateSqlBuilder
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); .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(" WHERE ml.loan_status_id = 300 "); updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" and mr.duedate >= CURDATE() "); updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" GROUP BY ml.id"); updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 ");
updateSqlBuilder.append(" HAVING (SUM(ifnull(mr.principal_completed_derived, 0)) + SUM(ifnull(mr.interest_completed_derived, 0)) +"); updateSqlBuilder.append(" and mr.duedate >= CURDATE() ");
updateSqlBuilder.append(" SUM(ifnull(mr.fee_charges_completed_derived, 0)) + SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0.0"); updateSqlBuilder.append(" GROUP BY ml.id");
updateSqlBuilder
int result = jdbcTemplate.update(updateSqlBuilder.toString()); .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");
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result); int result = jdbcTemplate.update(updateSqlBuilder.toString());
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
} }
@Transactional @Transactional
@Override @Override
@CronTargetMethod(jobName="Apply Annual Fee For Savings") @CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS)
public void applyAnnualFeeForSavings() { public void applyAnnualFeeForSavings() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
String sql = "SELECT ms.id as savingsAccountId FROM m_savings_account ms WHERE ms.annual_fee_next_due_date <= NOW() "; String sql = "SELECT ms.id as savingsAccountId FROM m_savings_account ms WHERE ms.annual_fee_next_due_date <= NOW() ";
List<Long> savingsIds = jdbcTemplate.queryForList(sql, Long.class); List<Long> savingsIds = jdbcTemplate.queryForList(sql, Long.class);
for (Long savingsId : savingsIds) { for (Long savingsId : savingsIds) {
this.savingsAccountWritePlatformService.applyAnnualFee(savingsId); this.savingsAccountWritePlatformService.applyAnnualFee(savingsId);
} }
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + savingsIds.size()); logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + savingsIds.size());
} }
} }