From f48f7ed0f562f3113335be314d10196080a54cf2 Mon Sep 17 00:00:00 2001 From: MithunKashyap Date: Tue, 9 Dec 2014 15:22:08 +0530 Subject: [PATCH] Mifos-1663:added more flexibilty for center/group meeting reschedule --- .../portfolio/calendar/CalendarConstants.java | 3 +- .../portfolio/calendar/domain/Calendar.java | 67 +++++++++++++++++ .../domain/CalendarFrequencyType.java | 4 + ...alendarCommandFromApiJsonDeserializer.java | 20 +++++ ...WritePlatformServiceJpaRepositoryImpl.java | 48 +++++++++++- .../portfolio/loanaccount/domain/Loan.java | 75 ++++++++++++++++++- .../service/LoanWritePlatformService.java | 5 +- ...WritePlatformServiceJpaRepositoryImpl.java | 28 ++++++- 8 files changed, 239 insertions(+), 11 deletions(-) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/CalendarConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/CalendarConstants.java index 96402c408..92bdca168 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/CalendarConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/CalendarConstants.java @@ -17,7 +17,8 @@ public class CalendarConstants { "location"), START_DATE("startDate"), END_DATE("endDate"), CREATED_DATE("createdDate"), DURATION("duration"), TYPE_ID( "typeId"), REPEATING("repeating"), REMIND_BY_ID("remindById"), FIRST_REMINDER("firstReminder"), SECOND_REMINDER( "secondReminder"), LOCALE("locale"), DATE_FORMAT("dateFormat"), FREQUENCY("frequency"), INTERVAL("interval"), REPEATS_ON_DAY( - "repeatsOnDay"); + "repeatsOnDay"), RESCHEDULE_BASED_ON_MEETING_DATES("reschedulebasedOnMeetingDates"), PRESENT_MEETING_DATE( + "presentMeetingDate"), NEW_MEETING_DATE("newMeetingDate"), ; private final String value; 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 11877313f..139d35a20 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 @@ -169,6 +169,58 @@ public class Calendar extends AbstractAuditableCustom { firstReminder, secondReminder); } + public Map updateStartDateAndDerivedFeilds(final LocalDate newMeetingStartDate) { + + final Map actualChanges = new LinkedHashMap<>(9); + + final LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + + if (newMeetingStartDate.isBefore(currentDate)) { + final String defaultUserMessage = "New meeting effective from date cannot be in past"; + throw new CalendarDateException("new.start.date.cannot.be.in.past", defaultUserMessage, newMeetingStartDate, + getStartDateLocalDate()); + } else if (isStartDateAfter(newMeetingStartDate) && isStartDateBeforeOrEqual(currentDate)) { + // new meeting date should be on or after start date or current + // date + final String defaultUserMessage = "New meeting effective from date cannot be a date before existing meeting start date"; + throw new CalendarDateException("new.start.date.before.existing.date", defaultUserMessage, newMeetingStartDate, + getStartDateLocalDate()); + } else { + + actualChanges.put(CALENDAR_SUPPORTED_PARAMETERS.START_DATE.getValue(), newMeetingStartDate.toString()); + this.startDate = newMeetingStartDate.toDate(); + + /* + * If meeting start date is changed then there is possibilities of + * recurring day may change, so derive the recurring day and update + * it if it is changed. For weekly type is weekday and for monthly + * type it is day of the month + */ + + CalendarFrequencyType calendarFrequencyType = CalendarUtils.getFrequency(this.recurrence); + Integer interval = CalendarUtils.getInterval(this.recurrence); + Integer repeatsOnDay = null; + + /* + * Repeats on day, need to derive based on the start date + */ + + if (calendarFrequencyType.isWeekly()) { + repeatsOnDay = newMeetingStartDate.getDayOfWeek(); + } else if (calendarFrequencyType.isMonthly()) { + repeatsOnDay = newMeetingStartDate.getDayOfMonth(); + } + + // TODO cover other recurrence also + + this.recurrence = constructRecurrence(calendarFrequencyType, interval, repeatsOnDay); + + } + + return actualChanges; + + } + public Map update(final JsonCommand command) { final Map actualChanges = new LinkedHashMap<>(9); @@ -276,6 +328,21 @@ public class Calendar extends AbstractAuditableCustom { final String newRecurrence = Calendar.constructRecurrence(command, this); if (!StringUtils.isBlank(this.recurrence) && !newRecurrence.equalsIgnoreCase(this.recurrence)) { + + // FIXME: AA - Is this restriction required only for collection type + // meetings or for all?. + // Do not allow to change meeting frequency + + if (!CalendarUtils.isFrequencySame(this.recurrence, newRecurrence)) { + final String defaultUserMessage = "Update of meeting frequency is not supported"; + throw new CalendarParameterUpdateNotSupportedException("meeting.frequency", defaultUserMessage); + } + + // Do not allow to change meeting interval + if (!CalendarUtils.isIntervalSame(this.recurrence, newRecurrence)) { + final String defaultUserMessage = "Update of meeting interval is not supported"; + throw new CalendarParameterUpdateNotSupportedException("meeting.interval", defaultUserMessage); + } actualChanges.put("recurrence", newRecurrence); this.recurrence = StringUtils.defaultIfEmpty(newRecurrence, null); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarFrequencyType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarFrequencyType.java index 8a87ea0ba..95a39a4ef 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarFrequencyType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarFrequencyType.java @@ -82,6 +82,10 @@ public enum CalendarFrequencyType { return this.value.equals(CalendarFrequencyType.WEEKLY.value); } + public boolean isMonthly() { + return this.value.equals(CalendarFrequencyType.MONTHLY.value); + } + public boolean isInvalid() { return this.value.equals(CalendarFrequencyType.INVALID.value); } 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 90e6ca8ec..f32634abc 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 @@ -201,6 +201,26 @@ public class CalendarCommandFromApiJsonDeserializer extends AbstractFromApiJsonD final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("calendar"); + if (this.fromApiJsonHelper.parameterExists(CALENDAR_SUPPORTED_PARAMETERS.RESCHEDULE_BASED_ON_MEETING_DATES.getValue(), element)) { + final Boolean rescheduleBasedOnMeetingDates = this.fromApiJsonHelper.extractBooleanNamed( + CALENDAR_SUPPORTED_PARAMETERS.RESCHEDULE_BASED_ON_MEETING_DATES.getValue(), element); + baseDataValidator.reset().parameter(CALENDAR_SUPPORTED_PARAMETERS.RESCHEDULE_BASED_ON_MEETING_DATES.getValue()) + .value(rescheduleBasedOnMeetingDates).validateForBooleanValue(); + } + + if (this.fromApiJsonHelper.parameterExists(CALENDAR_SUPPORTED_PARAMETERS.PRESENT_MEETING_DATE.getValue(), element)) { + final String presentMeetingDate = this.fromApiJsonHelper.extractStringNamed( + CALENDAR_SUPPORTED_PARAMETERS.PRESENT_MEETING_DATE.getValue(), element); + baseDataValidator.reset().parameter(CALENDAR_SUPPORTED_PARAMETERS.PRESENT_MEETING_DATE.getValue()).value(presentMeetingDate) + .notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(CALENDAR_SUPPORTED_PARAMETERS.NEW_MEETING_DATE.getValue(), element)) { + final String newMeetingDate = this.fromApiJsonHelper.extractStringNamed( + CALENDAR_SUPPORTED_PARAMETERS.NEW_MEETING_DATE.getValue(), element); + baseDataValidator.reset().parameter(CALENDAR_SUPPORTED_PARAMETERS.NEW_MEETING_DATE.getValue()).value(newMeetingDate).notNull(); + } + if (this.fromApiJsonHelper.parameterExists(CALENDAR_SUPPORTED_PARAMETERS.TITLE.getValue(), element)) { final String title = this.fromApiJsonHelper.extractStringNamed(CALENDAR_SUPPORTED_PARAMETERS.TITLE.getValue(), element); baseDataValidator.reset().parameter(CALENDAR_SUPPORTED_PARAMETERS.TITLE.getValue()).value(title).notBlank() 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 372e4895c..99ccdb4fb 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 @@ -177,12 +177,54 @@ public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWr final Calendar calendarForUpdate = this.calendarRepository.findOne(calendarId); if (calendarForUpdate == null) { throw new CalendarNotFoundException(calendarId); } + final Date oldStartDate = calendarForUpdate.getStartDate(); final LocalDate currentDate = DateUtils.getLocalDateOfTenant(); // create calendar history before updating calendar final CalendarHistory calendarHistory = new CalendarHistory(calendarForUpdate, oldStartDate); - final Map changes = calendarForUpdate.update(command); + Map changes = null; + + final Boolean reschedulebasedOnMeetingDates = command + .booleanObjectValueOfParameterNamed(CALENDAR_SUPPORTED_PARAMETERS.RESCHEDULE_BASED_ON_MEETING_DATES.getValue()); + + /* + * System allows to change the meeting date by two means, + * + * Option 1: reschedulebasedOnMeetingDates = false or reschedulebasedOnMeetingDates is not passed + * By directly editing the recurring day with effective from + * date and system decides the next meeting date based on some sensible + * logic (i.e., number of minimum days between two repayments) + * + * + * Option 2: reschedulebasedOnMeetingDates = true + * By providing alternative meeting date for one of future + * meeting date and derive the day of recurrence from the new meeting + * date. Ex: User proposes new meeting date say "14/Nov/2014" for + * present meeting date "12/Nov/2014", based on this input other values + * re derived and loans are rescheduled + * + */ + + LocalDate newMeetingDate = null; + LocalDate presentMeetingDate = null; + + if (reschedulebasedOnMeetingDates != null && reschedulebasedOnMeetingDates) { + + newMeetingDate = command.localDateValueOfParameterNamed(CALENDAR_SUPPORTED_PARAMETERS.NEW_MEETING_DATE.getValue()); + presentMeetingDate = command.localDateValueOfParameterNamed(CALENDAR_SUPPORTED_PARAMETERS.PRESENT_MEETING_DATE.getValue()); + + /* + * New meeting date proposed will become the new start date for the + * updated calendar + */ + + changes = calendarForUpdate.updateStartDateAndDerivedFeilds(newMeetingDate); + + } else { + changes = calendarForUpdate.update(command); + } + if (!changes.isEmpty()) { // update calendar history table only if there is a change in // calendar start date. @@ -202,7 +244,9 @@ public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWr if (!CollectionUtils.isEmpty(loanCalendarInstances)) { // update all loans associated with modifying calendar - this.loanWritePlatformService.applyMeetingDateChanges(calendarForUpdate, loanCalendarInstances); + this.loanWritePlatformService.applyMeetingDateChanges(calendarForUpdate, loanCalendarInstances, + reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate); + } } } 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 474f85692..e79e54826 100755 --- 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 @@ -3850,8 +3850,68 @@ public class Loan extends AbstractPersistable { return this.closedOnDate; } + public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, + final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, + final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) { + + // first repayment's from date is same as disbursement date. + /* + * meetingStartDate is used as seedDate Capture the seedDate from + * user and use the seedDate as meetingStart date + */ + + + + LocalDate tmpFromDate = getDisbursementDate(); + final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); + final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); + final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + + LocalDate newRepaymentDate = null; + Boolean isFirstTime = true; + + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { + + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + + if ( oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate) ) { + + + if(isFirstTime){ + + isFirstTime = false; + newRepaymentDate = newMeetingDate; + + }else{ + // tmpFromDate.plusDays(1) is done to make sure + // getNewRepaymentMeetingDate method returns next meeting + // date and not the same as tmpFromDate + newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, tmpFromDate, tmpFromDate.plusDays(1), + loanRepaymentInterval, frequency, workingDays); + } + + if (isHolidayEnabled) { + newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); + } + + loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); + // reset from date to get actual daysInPeriod + + if(!isFirstTime){ + loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); + } + + tmpFromDate = newRepaymentDate;// update with new repayment + // date + } else { + tmpFromDate = oldDueDate; + } + } + } + public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays) { + // first repayment's from date is same as disbursement date. LocalDate tmpFromDate = getDisbursementDate(); final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); @@ -3859,18 +3919,25 @@ public class Loan extends AbstractPersistable { final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); LocalDate newRepaymentDate = null; + LocalDate seedDate = meetingStartDate; + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { - final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + // FIXME: AA this won't update repayment dates before current date. - if (oldDueDate.isAfter(meetingStartDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant())) { - newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, + + + if ( oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant()) ) { + + newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, loanRepaymentInterval, frequency, workingDays); final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType, loanRepaymentInterval, tmpFromDate); if (newRepaymentDate.isAfter(maxDateLimitForNewRepayment)) { - newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate, + newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, seedDate, tmpFromDate, loanRepaymentInterval, frequency, workingDays); } 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 9653811c5..0b30c0ffb 100755 --- 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 @@ -57,7 +57,8 @@ public interface LoanWritePlatformService { CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command); - void applyMeetingDateChanges(Calendar calendar, Collection loanCalendarInstances); + void applyMeetingDateChanges(Calendar calendar, Collection loanCalendarInstances, + Boolean reschedulebasedOnMeetingDates, LocalDate presentMeetingDate, LocalDate newMeetingDate); void applyHolidaysToLoans(); @@ -83,4 +84,6 @@ public interface LoanWritePlatformService { CommandProcessingResult recoverFromGuarantor(Long loanId); + void applyMeetingDateChanges(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 0e35b6e2e..7097228c9 100755 --- 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 @@ -1968,11 +1968,25 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - + @Transactional @Override public void applyMeetingDateChanges(final Calendar calendar, final Collection loanCalendarInstances) { + final Boolean reschedulebasedOnMeetingDates = null; + final LocalDate presentMeetingDate = null; + final LocalDate newMeetingDate = null; + + applyMeetingDateChanges(calendar, loanCalendarInstances, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate); + + } + + + @Transactional + @Override + public void applyMeetingDateChanges(final Calendar calendar, final Collection loanCalendarInstances, + final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) { + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final WorkingDays workingDays = this.workingDaysRepository.findOne(); final Collection loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), @@ -1994,8 +2008,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new CalendarParameterUpdateNotSupportedException("jlg.loan.recalculation", defaultUserMessage); } holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate()); - loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, - holidays, workingDays); + + if(reschedulebasedOnMeetingDates != null && reschedulebasedOnMeetingDates ){ + loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, + holidays, workingDays, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate); + }else{ + loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, + holidays, workingDays); + } + + saveLoanWithDataIntegrityViolationChecks(loan); } }