diff --git a/mifosng-db/migrations/core_db/V71__insert_reschedule_repayment_to_configuration.sql b/mifosng-db/migrations/core_db/V71__insert_reschedule_repayment_to_configuration.sql new file mode 100644 index 000000000..e902697fa --- /dev/null +++ b/mifosng-db/migrations/core_db/V71__insert_reschedule_repayment_to_configuration.sql @@ -0,0 +1 @@ +INSERT INTO `c_configuration` (`name`, `enabled`) VALUES ('reschedule-future-repayments', 1); \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java index 81c4c9b17..45d8bc59c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -9,4 +9,5 @@ public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); boolean isAmazonS3Enabled(); + boolean isRescheduleFutureRepaymentsEnabled(); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 32107eca2..f9d861b1d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -44,4 +44,13 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService public boolean isAmazonS3Enabled(){ return this.globalConfigurationRepository.findOneByName("amazon-S3").isEnabled(); } + + @Override + public boolean isRescheduleFutureRepaymentsEnabled() { + final String rescheduleRepaymentsConfigurationProperty = "reschedule-future-repayments"; + final GlobalConfigurationProperty property = this.globalConfigurationRepository.findOneByName(rescheduleRepaymentsConfigurationProperty); + if (property == null) { throw new GlobalConfigurationPropertyNotFoundException(rescheduleRepaymentsConfigurationProperty); } + return property.isEnabled(); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/Calendar.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/Calendar.java index 28ece74a5..49395caf0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/Calendar.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/Calendar.java @@ -21,6 +21,9 @@ import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom; import org.mifosplatform.portfolio.calendar.CalendarConstants.CALENDAR_SUPPORTED_PARAMETERS; +import org.mifosplatform.portfolio.calendar.exception.CalendarDateException; +import org.mifosplatform.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException; +import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.useradministration.domain.AppUser; @Entity @@ -147,13 +150,19 @@ public class Calendar extends AbstractAuditableCustom { final String localeAsInput = command.locale(); final String startDateParamName = CALENDAR_SUPPORTED_PARAMETERS.START_DATE.getValue(); if (command.isChangeInLocalDateParameterNamed(startDateParamName, getStartDateLocalDate())) { - final String valueAsInput = command.stringValueOfParameterNamed(startDateParamName); - actualChanges.put(startDateParamName, valueAsInput); - actualChanges.put("dateFormat", dateFormatAsInput); - actualChanges.put("locale", localeAsInput); + final String valueAsInput = command.stringValueOfParameterNamed(startDateParamName); final LocalDate newValue = command.localDateValueOfParameterNamed(startDateParamName); - this.startDate = newValue.toDate(); + if(isStartDateBefore(newValue)){ + actualChanges.put(startDateParamName, valueAsInput); + actualChanges.put("dateFormat", dateFormatAsInput); + actualChanges.put("locale", localeAsInput); + this.startDate = newValue.toDate(); + }else{ + //new meeting start date should be greater than existing meeting start date + final String defaultUserMessage = "New meeting start on or after date cannot be a date before existing meeting start date"; + throw new CalendarDateException("new.start.date.before.existing.date", defaultUserMessage, newValue, getStartDateLocalDate()); + } } final String endDateParamName = CALENDAR_SUPPORTED_PARAMETERS.END_DATE.getValue(); @@ -174,13 +183,21 @@ public class Calendar extends AbstractAuditableCustom { this.duration = newValue; } + //Do not allow to change calendar type + //TODO: AA Instead of throwing an exception, do not allow meeting calendar type to update. final String typeParamName = CALENDAR_SUPPORTED_PARAMETERS.TYPE_ID.getValue(); if (command.isChangeInIntegerSansLocaleParameterNamed(typeParamName, this.typeId)) { final Integer newValue = command.integerValueSansLocaleOfParameterNamed(typeParamName); + final String defaultUserMessage = "Meeting calendar type update is not supported"; + final String oldMeeingType = CalendarType.fromInt(this.typeId).name(); + final String newMeetingType = CalendarType.fromInt(newValue).name(); + + throw new CalendarParameterUpdateNotSupportedException("meeting.type", defaultUserMessage, newMeetingType, oldMeeingType); + /*final Integer newValue = command.integerValueSansLocaleOfParameterNamed(typeParamName); actualChanges.put(typeParamName, newValue); - this.typeId = newValue; + this.typeId = newValue;*/ } - + final String repeatingParamName = CALENDAR_SUPPORTED_PARAMETERS.REPEATING.getValue(); if (command.isChangeInBooleanParameterNamed(repeatingParamName, this.repeating)) { final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(repeatingParamName); @@ -188,9 +205,25 @@ public class Calendar extends AbstractAuditableCustom { this.repeating = newValue; } + //FIXME: AA Construct recurrence rule in API rather than getting it from UI final String recurrenceParamName = CALENDAR_SUPPORTED_PARAMETERS.RECURRENCE.getValue(); if (command.isChangeInStringParameterNamed(recurrenceParamName, this.recurrence)) { final String newValue = command.stringValueOfParameterNamed(recurrenceParamName); + + //FIXME: AA - Is this restriction required only for collection type meetings or for all?. + //Do not allow to change meeting frequency + + if(!CalendarHelper.isFrequencySame(this.recurrence, newValue)){ + final String defaultUserMessage = "Update of meeting frequency is not supported"; + throw new CalendarParameterUpdateNotSupportedException("meeting.frequency", defaultUserMessage); + } + + //Do not allow to change meeting interval + if(!CalendarHelper.isIntervalSame(this.recurrence, newValue)){ + final String defaultUserMessage = "Update of meeting interval is not supported"; + throw new CalendarParameterUpdateNotSupportedException("meeting.interval", defaultUserMessage); + } + actualChanges.put(recurrenceParamName, newValue); this.recurrence = StringUtils.defaultIfEmpty(newValue, null); } @@ -283,4 +316,8 @@ public class Calendar extends AbstractAuditableCustom { return endDateLocalDate; } + public boolean isStartDateBefore(final LocalDate newStartDate) { + if (this.startDate != null && newStartDate != null && getStartDateLocalDate().isBefore(newStartDate)) { return true; } + return false; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java index 87fa2066a..f11547652 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java @@ -20,4 +20,6 @@ public interface CalendarInstanceRepository extends JpaRepository findByCalendarIdAndEntityTypeId(Long calendarId, Integer entityTypeId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarDateException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarDateException.java new file mode 100644 index 000000000..f87c2c43c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarDateException.java @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.calendar.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when Calendar resources are not found. + */ +public class CalendarDateException extends AbstractPlatformDomainRuleException { + + public CalendarDateException(String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { + super("error.msg.calendar." + postFix , defaultUserMessage, defaultUserMessageArgs); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarParameterUpdateNotSupportedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarParameterUpdateNotSupportedException.java new file mode 100644 index 000000000..21bca5984 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/CalendarParameterUpdateNotSupportedException.java @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.calendar.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when Calendar resources are not found. + */ +public class CalendarParameterUpdateNotSupportedException extends AbstractPlatformDomainRuleException { + + public CalendarParameterUpdateNotSupportedException(String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { + super("error.msg.calendar.update.of." + postFix + ".is.not.supported", defaultUserMessage, defaultUserMessageArgs); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/serialization/CalendarCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/serialization/CalendarCommandFromApiJsonDeserializer.java index a02b6b1a4..8edafed63 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/serialization/CalendarCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/serialization/CalendarCommandFromApiJsonDeserializer.java @@ -221,6 +221,7 @@ public class CalendarCommandFromApiJsonDeserializer extends AbstractFromApiJsonD CALENDAR_SUPPORTED_PARAMETERS.DURATION.getValue(), element); baseDataValidator.reset().parameter(CALENDAR_SUPPORTED_PARAMETERS.DURATION.getValue()).value(duration).ignoreIfNull(); } + //TODO: AA do not allow to change calendar type. if (this.fromApiJsonHelper.parameterExists(CALENDAR_SUPPORTED_PARAMETERS.TYPE_ID.getValue(), element)) { final Integer typeId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(CALENDAR_SUPPORTED_PARAMETERS.TYPE_ID.getValue(), element); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java index a85640223..967dc0b1f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java @@ -33,27 +33,30 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public class CalendarHelper { public static LocalDate getNextRecurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) { - final Recur recur = CalendarHelper.getICalRecur(recurringRule); - if (recur == null) { return null; } - + return getNextRecurringDate(recur, seedDate, startDate); + } + + private static LocalDate getNextRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate startDate) { final DateTime periodStart = new DateTime(startDate.toDate()); - final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded - final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate()); - - Date seed = null; - try { - seed = new Date(seedDateStr, "yyyy-MM-dd"); - } catch (ParseException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - + final Date seed = convertToiCal4JCompatibleDate(seedDate); final Date nextRecDate = recur.getNextDate(seed, periodStart); return nextRecDate == null ? null : new LocalDate(nextRecDate); } + + private static Date convertToiCal4JCompatibleDate(final LocalDate inputDate){ + // Date format in iCal4J is hard coded + Date formattedDate = null; + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + final String seedDateStr = df.format(inputDate.toDateTimeAtStartOfDay().toDate()); + try { + formattedDate = new Date(seedDateStr, "yyyy-MM-dd"); + } catch (ParseException e) { + e.printStackTrace(); + } + return formattedDate; + } public static Collection getRecurringDates(final String recurringRule, final LocalDate seedDate, final LocalDate endDate) { @@ -79,16 +82,14 @@ public class CalendarHelper { final Recur recur = CalendarHelper.getICalRecur(recurringRule); + return getRecurringDates(recur, seedDate, periodStartDate, + periodEndDate, maxCount); + } + + private static Collection getRecurringDates(final Recur recur, final LocalDate seedDate, final LocalDate periodStartDate, + final LocalDate periodEndDate, final int maxCount) { if (recur == null) { return null; } - final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate()); - Date seed = null; - try { - seed = new Date(seedDateStr, "yyyy-MM-dd"); - } catch (ParseException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + final Date seed = convertToiCal4JCompatibleDate(seedDate); final DateTime periodStart = new DateTime(periodStartDate.toDate()); final DateTime periodEnd = new DateTime(periodEndDate.toDate()); @@ -207,7 +208,15 @@ public class CalendarHelper { public static boolean isValidRedurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate date){ - final Collection recurDate = getRecurringDates(recurringRule, seedDate, date, date.plusDays(1), 1); + final Recur recur = CalendarHelper.getICalRecur(recurringRule); + if (recur == null) { return false; } + + return isValidRedurringDate(recur, seedDate, date); + } + + public static boolean isValidRedurringDate(final Recur recur, final LocalDate seedDate, final LocalDate date){ + + final Collection recurDate = getRecurringDates(recur, seedDate, date, date.plusDays(1), 1); return (recurDate == null || recurDate.isEmpty()) ? false : true; } @@ -287,22 +296,50 @@ public class CalendarHelper { if(recur.getFrequency().equals(Recur.DAILY)){ recur.setFrequency(frequency); } - - final DateTime periodStart = new DateTime(startDate.toDate()); - final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded - final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate()); - - Date seed = null; - try { - seed = new Date(seedDateStr, "yyyy-MM-dd"); - } catch (ParseException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - final Date nextRecDate = recur.getNextDate(seed, periodStart); - final LocalDate firstRepaymentDate = nextRecDate == null ? null : new LocalDate(nextRecDate); + + final LocalDate firstRepaymentDate = getNextRecurringDate(recur, seedDate, startDate); return firstRepaymentDate; } + + public static LocalDate getNewRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate oldRepaymentDate, final Integer loanRepaymentInterval, final String frequency){ + final Recur recur = CalendarHelper.getICalRecur(recurringRule); + if (recur == null) { return null; } + + if(isValidRedurringDate(recur, seedDate, oldRepaymentDate)){ + return oldRepaymentDate; + } + /* Recurring dates should follow loanRepaymentInterval. + e.g. The weekly meeting will have interval of 1, if the loan product with fortnightly frequency will have interval of 2, + to generate right set of meeting dates reset interval same as loan repayment interval. + */ + recur.setInterval(loanRepaymentInterval); + + /*Recurring dates should follow loanRepayment frequency. + //e.g. daily meeting frequency should support all loan products with any type of frequency. + to generate right set of meeting dates reset frequency same as loan repayment frequency. + */ + if(recur.getFrequency().equals(Recur.DAILY)){ + recur.setFrequency(frequency); + } + + final LocalDate newRepaymentDate = getNextRecurringDate(recur, seedDate, oldRepaymentDate); + return newRepaymentDate; + } + + public static boolean isFrequencySame(final String oldRRule, final String newRRule) { + Recur oldRecur = getICalRecur(oldRRule); + Recur newRecur = getICalRecur(newRRule); + + if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) { return false; } + return oldRecur.getFrequency().equals(newRecur.getFrequency()); + } + + public static boolean isIntervalSame(final String oldRRule, final String newRRule) { + Recur oldRecur = getICalRecur(oldRRule); + Recur newRecur = getICalRecur(newRRule); + + if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) { return false; } + return (oldRecur.getInterval() == newRecur.getInterval()); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarWritePlatformServiceJpaRepositoryImpl.java index 3752ea4a0..84f9116d3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarWritePlatformServiceJpaRepositoryImpl.java @@ -5,19 +5,24 @@ */ package org.mifosplatform.portfolio.calendar.service; +import java.util.Collection; import java.util.Map; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; import org.mifosplatform.portfolio.calendar.serialization.CalendarCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; @Service public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWritePlatformService { @@ -25,14 +30,18 @@ public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWr private final CalendarRepository calendarRepository; private final CalendarCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final CalendarInstanceRepository calendarInstanceRepository; + private final LoanWritePlatformService loanWritePlatformService; + private final ConfigurationDomainService configurationDomainService; @Autowired public CalendarWritePlatformServiceJpaRepositoryImpl(final CalendarRepository calendarRepository, final CalendarCommandFromApiJsonDeserializer fromApiJsonDeserializer, - final CalendarInstanceRepository calendarInstanceRepository) { + final CalendarInstanceRepository calendarInstanceRepository, final LoanWritePlatformService loanWritePlatformService, final ConfigurationDomainService configurationDomainService) { this.calendarRepository = calendarRepository; this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.calendarInstanceRepository = calendarInstanceRepository; + this.loanWritePlatformService = loanWritePlatformService; + this.configurationDomainService = configurationDomainService; } @Override @@ -66,6 +75,18 @@ public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWr if (!changes.isEmpty()) { this.calendarRepository.saveAndFlush(calendarForUpdate); + + if(this.configurationDomainService.isRescheduleFutureRepaymentsEnabled()){ + //In this approach loans following this meeting calendar will be affected immediately. + //get all calendar instances following this meeting calendar and calendar entity type is loan + final Collection loanCalendarInstances = this.calendarInstanceRepository.findByCalendarIdAndEntityTypeId(calendarId, CalendarEntityType.LOANS.getValue()); + + if (!CollectionUtils.isEmpty(loanCalendarInstances)) { + // update all loans which are following this meeting calendar + this.loanWritePlatformService.updateRepaymentsSchedule(calendarForUpdate, loanCalendarInstances); + // + } + } } return new CommandProcessingResultBuilder() // diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index cf195ef33..0c0fe41cb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -51,6 +51,7 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.staff.domain.Staff; +import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedException; import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.collateral.data.CollateralData; @@ -190,7 +191,6 @@ public class Loan extends AbstractPersistable { @JoinColumn(name = "disbursedon_userid", nullable = true) private AppUser disbursedBy; - @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "closedon_date") private Date closedOnDate; @@ -2325,4 +2325,28 @@ public class Loan extends AbstractPersistable { return this.closedOnDate; } + public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule) { + //update repayment dates of repayment schedule installaments + LocalDate tmpFromDate = this.getDisbursementDate();//first repayment's from date is same as disbursement date. + LocalDate newRepaymentDate = null; + for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { + final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + //FIXME: AA this won't update repayment dates before current date. + if (oldDueDate.isAfter(meetingStartDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant())) { + final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); + final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); + // reset repayment date with new meeting date + newRepaymentDate = CalendarHelper.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, loanRepaymentInterval, frequency); + loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); + //reset from date to get actual daysInPeriod + loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); + tmpFromDate = newRepaymentDate;//update with new repayment date + }else{ + tmpFromDate = oldDueDate; + } + + } + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 05ff88a47..12f066920 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -35,11 +35,11 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus @Temporal(TemporalType.DATE) @Column(name = "fromdate", nullable = true) - private final Date fromDate; + private Date fromDate; @Temporal(TemporalType.DATE) @Column(name = "duedate", nullable = false) - private final Date dueDate; + private Date dueDate; @Column(name = "principal_amount", scale = 6, precision = 19, nullable = true) private BigDecimal principal; @@ -521,4 +521,16 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.obligationsMetOnDate = transactionDate.toDate(); } } + + public void updateDueDate(final LocalDate newDueDate){ + if(newDueDate != null){ + this.dueDate = newDueDate.toDate(); + } + } + + public void updateFromDate(final LocalDate newFromDate){ + if(newFromDate != null){ + this.fromDate = newFromDate.toDate(); + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java index 542aa867a..a72e53ee8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.loanaccount.domain; +import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -19,4 +20,7 @@ public interface LoanRepository extends JpaRepository, @Query("from Loan loan where loan.client.id = :clientId") List findLoanByClientId(@Param("clientId") Long clientId); + + @Query("from Loan loan where loan.id IN :ids and loan.loanStatus IN :loanStatuses and loan.loanType IN :loanTypes") + List findByIdsAndLoanStatusAndLoanType(@Param("ids") Collection ids, @Param("loanStatuses") Collection loanStatuses, @Param("loanTypes") Collection loanTypes); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index 9c18b7ef8..153ba90ce 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -5,10 +5,13 @@ */ package org.mifosplatform.portfolio.loanaccount.service; +import java.util.Collection; import java.util.Map; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; @@ -47,4 +50,6 @@ public interface LoanWritePlatformService { CommandProcessingResult bulkLoanReassignment(JsonCommand command); CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command); + + void updateRepaymentsSchedule(Calendar calendar, Collection loanCalendarInstances); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 317c4167b..c092435d8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -8,6 +8,7 @@ package org.mifosplatform.portfolio.loanaccount.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -55,6 +56,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.mifosplatform.portfolio.loanaccount.domain.LoanType; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; @@ -982,4 +984,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingTransactionIds, existingReversedTransactionIds); journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } + + @Override + public void updateRepaymentsSchedule(final Calendar calendar, Collection loanCalendarInstances) { + + final Collection loanStatuses = new ArrayList(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), + LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue())); + final Collection loanTypes = new ArrayList(Arrays.asList(LoanType.GROUP.getValue(), LoanType.JLG.getValue())); + final Collection loanIds = new ArrayList(loanCalendarInstances.size()); + //loop through loanCalendarInstances to get loan ids + for (CalendarInstance calendarInstance : loanCalendarInstances) { + loanIds.add(calendarInstance.getEntityId()); + } + + final List loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes); + //loop through each loan to reschedule the repayment dates + for (Loan loan : loans) { + if(loan != null){ + loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence()); + this.loanRepository.save(loan); + } + } + } } \ No newline at end of file