Merge pull request #457 from ashok-conflux/MIFOSX-543

MIFOSX-543: Implement working days for Loans
This commit is contained in:
Vishwas Babu A J
2013-07-17 04:02:53 -07:00
20 changed files with 475 additions and 94 deletions
@@ -0,0 +1,11 @@
CREATE TABLE `m_working_days` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`recurrence` VARCHAR(100) NULL DEFAULT NULL,
`repayment_rescheduling_enum` SMALLINT(5) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1;
INSERT INTO `m_working_days` (`recurrence`, `repayment_rescheduling_enum`) VALUES ('FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA', 2);
@@ -17,4 +17,14 @@ public class HolidayUtil {
} }
return repaymentDate; return repaymentDate;
} }
public static boolean isHoliday(final LocalDate date, final List<Holiday> holidays){
for (Holiday holiday : holidays) {
if (date.isEqual(holiday.getFromDateLocalDate()) || date.isEqual(holiday.getToDateLocalDate()) || (date.isAfter(holiday.getFromDateLocalDate()) && date.isBefore(holiday.getToDateLocalDate()))) {
return true;
}
}
return false;
}
} }
@@ -113,14 +113,14 @@ public class HolidayWritePlatformServiceJpaRepositoryImpl implements HolidayWrit
throw new HolidayDateException("to.date.cannot.be.before.from.date", defaultUserMessage, fromDate.toString(), toDate.toString()); throw new HolidayDateException("to.date.cannot.be.before.from.date", defaultUserMessage, fromDate.toString(), toDate.toString());
} }
//validate alternative working date //validate repaymentsRescheduledTo date
//1. should be within a 7 days date range. //1. should be within a 7 days date range.
//2. Alternative date should not be an exist holiday.//TBD //2. Alternative date should not be an exist holiday.//TBD
//3. Holiday should not be on an alternate working date of another holiday.//TBD //3. Holiday should not be on an repaymentsRescheduledTo date of another holiday.//TBD
//restricting alternative working date to be within 7 days range before or after from date and to date. //restricting repaymentsRescheduledTo date to be within 7 days range before or after from date and to date.
if(repaymentsRescheduledTo.isBefore(fromDate.minusDays(7)) || repaymentsRescheduledTo.isAfter(toDate.plusDays(7))){ if(repaymentsRescheduledTo.isBefore(fromDate.minusDays(7)) || repaymentsRescheduledTo.isAfter(toDate.plusDays(7))){
final String defaultUserMessage = "Alternative working date must be within 7 days before or after from and to dates"; final String defaultUserMessage = "Repayments Rescheduled to date must be within 7 days before or after from and to dates";
throw new HolidayDateException("repayments.rescheduled.to.must.be.within.range", defaultUserMessage, fromDate.toString(), toDate.toString(), repaymentsRescheduledTo.toString()); throw new HolidayDateException("repayments.rescheduled.to.must.be.within.range", defaultUserMessage, fromDate.toString(), toDate.toString(), repaymentsRescheduledTo.toString());
} }
} }
@@ -0,0 +1,72 @@
/**
* 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.organisation.workingdays.domain;
import java.util.HashMap;
import java.util.Map;
public enum RepaymentRescheduleType {
INVALID(0, "RepaymentRescheduleType.invalid"),
SAME_DAY(1, "RepaymentRescheduleType.same.day"),
MOVE_TO_NEXT_WORKING_DAY(2,"RepaymentRescheduleType.move.to.next.working.day"),
MOVE_TO_NEXT_REPAYMENT_MEETING_DAY(3,"RepaymentRescheduleType.move.to.next.repayment.meeting.day"),
MOVE_TO_PREVIOUS_WORKING_DAY(4,"RepaymentRescheduleType.move.to.previous.working.day");
private final Integer value;
private final String code;
private RepaymentRescheduleType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
private static final Map<Integer, RepaymentRescheduleType> intToEnumMap = new HashMap<Integer, RepaymentRescheduleType>();
private static int minValue;
private static int maxValue;
static {
int i = 0;
for (final RepaymentRescheduleType entityType : RepaymentRescheduleType.values()) {
if (i == 0) {
minValue = entityType.value;
}
intToEnumMap.put(entityType.value, entityType);
if (minValue >= entityType.value) {
minValue = entityType.value;
}
if (maxValue < entityType.value) {
maxValue = entityType.value;
}
i = i + 1;
}
}
public static RepaymentRescheduleType fromInt(final int i) {
final RepaymentRescheduleType RepaymentRescheduleType = intToEnumMap.get(Integer.valueOf(i));
return RepaymentRescheduleType;
}
public static int getMinValue() {
return minValue;
}
public static int getMaxValue() {
return maxValue;
}
@Override
public String toString() {
return name().toString();
}
}
@@ -0,0 +1,50 @@
/**
* 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.organisation.workingdays.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "m_working_days")
public class WorkingDays extends AbstractPersistable<Long> {
@Column(name = "recurrence", length = 100, nullable = true)
private String recurrence;
@Column(name = "repayment_rescheduling_enum", nullable = false)
private Integer repaymentReschedulingType;
protected WorkingDays() {
}
protected WorkingDays(String recurrence, Integer repaymentReschedulingType) {
this.recurrence = recurrence;
this.repaymentReschedulingType = repaymentReschedulingType;
}
/**
* @return the recurrence
*/
public String getRecurrence() {
return this.recurrence;
}
/**
* @return the repaymentReschedulingType
*/
public Integer getRepaymentReschedulingType() {
return this.repaymentReschedulingType;
}
}
@@ -0,0 +1,13 @@
/**
* 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.organisation.workingdays.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface WorkingDaysRepository extends JpaRepository<WorkingDays, Long>, JpaSpecificationExecutor<WorkingDays> {
}
@@ -0,0 +1,50 @@
/**
* 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.organisation.workingdays.domain;
import java.util.List;
import org.mifosplatform.organisation.workingdays.exception.WorkingDaysNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* Wrapper for {@link WorkingDaysRepository} that adds NULL checking and Error
* handling capabilities
* </p>
*/
@Service
public class WorkingDaysRepositoryWrapper {
private final WorkingDaysRepository repository;
@Autowired
public WorkingDaysRepositoryWrapper(final WorkingDaysRepository repository) {
this.repository = repository;
}
public WorkingDays findOne() {
final List<WorkingDays> workingDaysList = this.repository.findAll();
if (workingDaysList == null || workingDaysList.isEmpty()) {
throw new WorkingDaysNotFoundException();
}
return workingDaysList.get(0);
}
public void save(final WorkingDays workingDays) {
this.repository.save(workingDays);
}
public void saveAndFlush(final WorkingDays workingDays) {
this.repository.saveAndFlush(workingDays);
}
public void delete(final WorkingDays workingDays) {
this.repository.delete(workingDays);
}
}
@@ -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.organisation.workingdays.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* A {@link RuntimeException} thrown when Calendar resources are not found.
*/
public class WorkingDaysNotFoundException extends AbstractPlatformResourceNotFoundException {
public WorkingDaysNotFoundException() {
super("error.msg.working.days.not.configured", "Must configure the Working days for the organisation.");
}
}
@@ -0,0 +1,43 @@
/**
* 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.organisation.workingdays.service;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.workingdays.domain.RepaymentRescheduleType;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
public class WorkingDaysUtil {
public static LocalDate getOffSetDateIfNonWorkingDay(final LocalDate date, final LocalDate nextMeetingDate, final WorkingDays workingDays) {
//If date is not a non working day then return date.
if (isWorkingDay(workingDays, date)) { return date; }
final RepaymentRescheduleType rescheduleType = RepaymentRescheduleType.fromInt(workingDays
.getRepaymentReschedulingType());
switch (rescheduleType) {
case INVALID:
return date;
case SAME_DAY:
return date;
case MOVE_TO_NEXT_WORKING_DAY:
return getOffSetDateIfNonWorkingDay(date.plusDays(1), nextMeetingDate, workingDays);
case MOVE_TO_NEXT_REPAYMENT_MEETING_DAY:
return nextMeetingDate;
case MOVE_TO_PREVIOUS_WORKING_DAY:
return getOffSetDateIfNonWorkingDay(date.minusDays(1), nextMeetingDate, workingDays);
default:
return date;
}
}
public static boolean isWorkingDay(WorkingDays workingDays, final LocalDate date){
return CalendarHelper.isValidRedurringDate(workingDays.getRecurrence(), date, date);
}
}
@@ -27,6 +27,8 @@ import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
@@ -171,7 +173,7 @@ public class CalendarHelper {
for (@SuppressWarnings("rawtypes") for (@SuppressWarnings("rawtypes")
final Iterator iterator = weekDayList.iterator(); iterator.hasNext();) { final Iterator iterator = weekDayList.iterator(); iterator.hasNext();) {
final WeekDay weekDay = (WeekDay) iterator.next(); final WeekDay weekDay = (WeekDay) iterator.next();
humanReadable += DayNameEnum.from(weekDay.getDay()).getValue(); humanReadable += DayNameEnum.from(weekDay.getDay()).getCode();
} }
} else if (recur.getFrequency().equals(Recur.MONTHLY)) { } else if (recur.getFrequency().equals(Recur.MONTHLY)) {
@@ -211,30 +213,35 @@ public class CalendarHelper {
final Recur recur = CalendarHelper.getICalRecur(recurringRule); final Recur recur = CalendarHelper.getICalRecur(recurringRule);
if (recur == null) { return false; } if (recur == null) { return false; }
return isValidRedurringDate(recur, seedDate, date); return isValidRecurringDate(recur, seedDate, date);
} }
public static boolean isValidRedurringDate(final Recur recur, final LocalDate seedDate, final LocalDate date){ public static boolean isValidRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate date){
final Collection<LocalDate> recurDate = getRecurringDates(recur, seedDate, date, date.plusDays(1), 1); final Collection<LocalDate> recurDate = getRecurringDates(recur, seedDate, date, date.plusDays(1), 1);
return (recurDate == null || recurDate.isEmpty()) ? false : true; return (recurDate == null || recurDate.isEmpty()) ? false : true;
} }
public static enum DayNameEnum{ public static enum DayNameEnum{
MO("Monday"),TU("Tuesday"),WE("Wednesday"),TH("Thursday"),FR("Friday"),SA("Saturday"),SU("Sunday"); MO(1, "Monday"), TU(2, "Tuesday"), WE(3, "Wednesday"), TH(4, "Thursday"), FR(5, "Friday"), SA(6, "Saturday"), SU(7, "Sunday");
private final String value; private final String code;
private final Integer value;
private DayNameEnum(String value){ private DayNameEnum(final Integer value, final String code){
this.value = value; this.value = value;
this.code = code;
} }
public String getValue(){ public String getCode(){
return this.code;
}
public int getValue() {
return this.value; return this.value;
} }
public static DayNameEnum from(String code){ public static DayNameEnum from(String name){
for (DayNameEnum dayName : DayNameEnum.values()){ for (DayNameEnum dayName : DayNameEnum.values()){
if(dayName.toString().equals(code)) return dayName; if(dayName.toString().equals(name)) return dayName;
} }
return DayNameEnum.MO;//Default it to Monday return DayNameEnum.MO;//Default it to Monday
} }
@@ -302,11 +309,11 @@ public class CalendarHelper {
return firstRepaymentDate; return firstRepaymentDate;
} }
public static LocalDate getNewRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate oldRepaymentDate, final Integer loanRepaymentInterval, final String frequency){ public static LocalDate getNewRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate oldRepaymentDate, final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays){
final Recur recur = CalendarHelper.getICalRecur(recurringRule); final Recur recur = CalendarHelper.getICalRecur(recurringRule);
if (recur == null) { return null; } if (recur == null) { return null; }
if(isValidRedurringDate(recur, seedDate, oldRepaymentDate)){ if(isValidRecurringDate(recur, seedDate, oldRepaymentDate)){
return oldRepaymentDate; return oldRepaymentDate;
} }
/* Recurring dates should follow loanRepaymentInterval. /* Recurring dates should follow loanRepaymentInterval.
@@ -323,7 +330,11 @@ public class CalendarHelper {
recur.setFrequency(frequency); recur.setFrequency(frequency);
} }
final LocalDate newRepaymentDate = getNextRecurringDate(recur, seedDate, oldRepaymentDate); LocalDate newRepaymentDate = getNextRecurringDate(recur, seedDate, oldRepaymentDate);
LocalDate nextRepaymentDate = getNextRecurringDate(recur, seedDate, newRepaymentDate);
newRepaymentDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDate, nextRepaymentDate, workingDays);
return newRepaymentDate; return newRepaymentDate;
} }
@@ -83,7 +83,7 @@ public class CalendarWritePlatformServiceJpaRepositoryImpl implements CalendarWr
if (!CollectionUtils.isEmpty(loanCalendarInstances)) { if (!CollectionUtils.isEmpty(loanCalendarInstances)) {
// update all loans which are following this meeting calendar // update all loans which are following this meeting calendar
this.loanWritePlatformService.updateRepaymentsSchedule(calendarForUpdate, loanCalendarInstances); this.loanWritePlatformService.applyMeetingDateChanges(calendarForUpdate, loanCalendarInstances);
// //
} }
} }
@@ -53,6 +53,8 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedException; import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedException;
import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.Client;
@@ -64,6 +66,7 @@ import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
@@ -1072,7 +1075,8 @@ public class Loan extends AbstractPersistable<Long> {
public void loanApplicationSubmittal(final AppUser currentUser, final LoanScheduleModel loanSchedule, public void loanApplicationSubmittal(final AppUser currentUser, final LoanScheduleModel loanSchedule,
final LoanApplicationTerms loanApplicationTerms, final LoanLifecycleStateMachine lifecycleStateMachine, final LoanApplicationTerms loanApplicationTerms, final LoanLifecycleStateMachine lifecycleStateMachine,
final LocalDate submittedOn, final String externalId) { final LocalDate submittedOn, final String externalId, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays) {
updateLoanSchedule(loanSchedule); updateLoanSchedule(loanSchedule);
@@ -1119,8 +1123,12 @@ public class Loan extends AbstractPersistable<Long> {
for (LoanCharge loanCharge : setOfLoanCharges()) { for (LoanCharge loanCharge : setOfLoanCharges()) {
validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate()); validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate());
} }
//validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays);
validateDisbursementDateIsOnHoliday(isHolidayEnabled, holidays);
} }
private LocalDate determineExpectedMaturityDate() { private LocalDate determineExpectedMaturityDate() {
int numberOfInstallments = this.repaymentScheduleInstallments.size(); int numberOfInstallments = this.repaymentScheduleInstallments.size();
return this.repaymentScheduleInstallments.get(numberOfInstallments - 1).getDueDate(); return this.repaymentScheduleInstallments.get(numberOfInstallments - 1).getDueDate();
@@ -1309,7 +1317,7 @@ public class Loan extends AbstractPersistable<Long> {
public Map<String, Object> disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, public Map<String, Object> disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser,
final JsonCommand command, final ApplicationCurrency currency, final List<Long> existingTransactionIds, final JsonCommand command, final ApplicationCurrency currency, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final Map<String, Object> actualChanges, final PaymentDetail paymentDetail, final List<Long> existingReversedTransactionIds, final Map<String, Object> actualChanges, final PaymentDetail paymentDetail,
final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, final List<Holiday> holidays) { final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
updateLoanToPreDisbursalState(); updateLoanToPreDisbursalState();
@@ -1332,10 +1340,14 @@ public class Loan extends AbstractPersistable<Long> {
actualChanges.put("dateFormat", command.dateFormat()); actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate")); actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate"));
//validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays);
validateDisbursementDateIsOnHoliday(isHolidayEnabled, holidays);
handleDisbursementTransaction(paymentDetail, actualDisbursementDate); handleDisbursementTransaction(paymentDetail, actualDisbursementDate);
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) {
regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate, isHolidayEnabled, holidays); regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate, isHolidayEnabled, holidays, workingDays);
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
} else { } else {
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
@@ -1363,7 +1375,7 @@ public class Loan extends AbstractPersistable<Long> {
*/ */
private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled,
final List<Holiday> holidays) { final List<Holiday> holidays, final WorkingDays workingDays) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
@@ -1382,8 +1394,7 @@ public class Loan extends AbstractPersistable<Long> {
loanTermPeriodFrequencyType, getDisbursementDate(), firstRepaymentOnDate, getInArrearsTolerance(), loanTermPeriodFrequencyType, getDisbursementDate(), firstRepaymentOnDate, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail); this.loanRepaymentScheduleDetail);
LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges, LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges, isHolidayEnabled, holidays, workingDays);
isHolidayEnabled, holidays);
updateLoanSchedule(loanSchedule); updateLoanSchedule(loanSchedule);
} }
@@ -1508,8 +1519,12 @@ public class Loan extends AbstractPersistable<Long> {
public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction, public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds, final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) { final List<Long> existingReversedTransactionIds, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays) {
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), isHolidayEnabled, holidays);
validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), workingDays);
existingTransactionIds.addAll(findExistingTransactionIds()); existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
@@ -2346,12 +2361,9 @@ public class Loan extends AbstractPersistable<Long> {
} }
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays) { final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
// update repayment dates of repayment schedule installaments //update repayment dates of repayment schedule installaments
LocalDate tmpFromDate = this.getDisbursementDate();// first repayment's LocalDate tmpFromDate = this.getDisbursementDate();//first repayment's from date is same as disbursement date.
// from date is same
// as disbursement
// date.
LocalDate newRepaymentDate = null; LocalDate newRepaymentDate = null;
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) { for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) {
final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
@@ -2360,12 +2372,13 @@ public class Loan extends AbstractPersistable<Long> {
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery();
// reset repayment date with new meeting date // reset repayment date with new meeting date
newRepaymentDate = CalendarHelper.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, newRepaymentDate = CalendarHelper.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, loanRepaymentInterval, frequency, workingDays);
loanRepaymentInterval, frequency); if(isHolidayEnabled){
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
} }
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod // reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
@@ -2393,7 +2406,8 @@ public class Loan extends AbstractPersistable<Long> {
if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.equals(holiday.getToDateLocalDate()) if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.equals(holiday.getToDateLocalDate())
|| (oldDueDate.isAfter(holiday.getFromDateLocalDate()) && oldDueDate.isBefore(holiday.getToDateLocalDate()))) { || (oldDueDate.isAfter(holiday.getFromDateLocalDate()) && oldDueDate.isBefore(holiday.getToDateLocalDate()))) {
// should be take from holiday //FIXME: AA do we need to apply non-working days.
//Assuming holiday's repayment reschedule to date cannot be created on a non-working day.
final LocalDate newRepaymentDate = holiday.getRepaymentsRescheduledToLocalDate(); final LocalDate newRepaymentDate = holiday.getRepaymentsRescheduledToLocalDate();
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
} }
@@ -2401,6 +2415,39 @@ public class Loan extends AbstractPersistable<Long> {
} }
} }
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays) {
if(!WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())){
final String errorMessage = "Expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage, getExpectedDisbursedOnLocalDate());
}
}
private void validateDisbursementDateIsOnHoliday(final boolean isHolidayEnabled, final List<Holiday> holidays) {
if (isHolidayEnabled) {
if (HolidayUtil.isHoliday(getDisbursementDate(), holidays)) {
final String errorMessage = "Expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage,
getExpectedDisbursedOnLocalDate());
}
}
}
private void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays) {
if(!WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)){
final String errorMessage = "Repayment date cannot be on a non working day";
throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
}
}
private void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean isHolidayEnabled, final List<Holiday> holidays) {
if (isHolidayEnabled) {
if (HolidayUtil.isHoliday(repaymentDate, holidays)) {
final String errorMessage = "Repayment date cannot be on a holiday";
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
}
}
}
public Group group() { public Group group() {
return this.group; return this.group;
} }
@@ -18,6 +18,7 @@ import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
/** /**
@@ -30,10 +31,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
@Override @Override
public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency, public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final List<Holiday> holidays) { final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays) {
// 1. generate list of proposed schedule due dates // 1. generate list of proposed schedule due dates
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms, isHolidayEnabled, holidays); final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms, isHolidayEnabled, holidays, workingDays);
// 2. determine the total charges due at time of disbursement // 2. determine the total charges due at time of disbursement
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
@@ -11,13 +11,15 @@ import java.util.List;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.service.HolidayUtil; import org.mifosplatform.organisation.holiday.service.HolidayUtil;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
@Override @Override
public List<LocalDate> generate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, public List<LocalDate> generate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled,
final List<Holiday> holidays) { final List<Holiday> holidays, final WorkingDays workingDays) {
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
@@ -29,44 +31,49 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
LocalDate dueRepaymentPeriodDate = startDate; LocalDate dueRepaymentPeriodDate = startDate;
if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) {
dueRepaymentPeriodDate = firstRepaymentPeriodDate; dueRepaymentPeriodDate = firstRepaymentPeriodDate;
} else { } else {
dueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, startDate);
final int repaidEvery = loanApplicationTerms.getRepaymentEvery();
switch (loanApplicationTerms.getRepaymentPeriodFrequencyType()) {
case DAYS:
dueRepaymentPeriodDate = startDate.plusDays(repaidEvery);
break;
case WEEKS:
dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery);
break;
case MONTHS:
dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery);
break;
case YEARS:
dueRepaymentPeriodDate = startDate.plusYears(repaidEvery);
break;
case INVALID:
break;
}
} }
//start date should be assigned before applying holidays to avoid the reset of next repayment dates.
startDate = dueRepaymentPeriodDate; startDate = dueRepaymentPeriodDate;
//Apply holidays LocalDate nextDueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, dueRepaymentPeriodDate);
dueRepaymentPeriodDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(dueRepaymentPeriodDate, nextDueRepaymentPeriodDate, workingDays);
if(isHolidayEnabled){ if(isHolidayEnabled){
dueRepaymentPeriodDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(dueRepaymentPeriodDate, holidays); dueRepaymentPeriodDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(dueRepaymentPeriodDate, holidays);
} }
dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); dueRepaymentPeriodDates.add(dueRepaymentPeriodDate);
} }
return dueRepaymentPeriodDates; return dueRepaymentPeriodDates;
} }
private LocalDate getRepaymentPeriodDate(final LoanApplicationTerms loanApplicationTerms, LocalDate startDate) {
final int repaidEvery = loanApplicationTerms.getRepaymentEvery();
LocalDate dueRepaymentPeriodDate = startDate;
switch (loanApplicationTerms.getRepaymentPeriodFrequencyType()) {
case DAYS:
dueRepaymentPeriodDate = startDate.plusDays(repaidEvery);
break;
case WEEKS:
dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery);
break;
case MONTHS:
dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery);
break;
case YEARS:
dueRepaymentPeriodDate = startDate.plusYears(repaidEvery);
break;
case INVALID:
break;
}
return dueRepaymentPeriodDate;
}
@Override @Override
public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(final PeriodFrequencyType repaymentPeriodFrequencyType, public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(final PeriodFrequencyType repaymentPeriodFrequencyType,
final int repaidEvery, final List<LocalDate> scheduledDates) { final int repaidEvery, final List<LocalDate> scheduledDates) {
@@ -11,10 +11,11 @@ import java.util.Set;
import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
public interface LoanScheduleGenerator { public interface LoanScheduleGenerator {
LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms, LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms,
Set<LoanCharge> loanCharges, boolean isHolidayEnabled, List<Holiday> holidays); Set<LoanCharge> loanCharges, boolean isHolidayEnabled, List<Holiday> holidays, final WorkingDays workingDays);
} }
@@ -9,11 +9,12 @@ import java.util.List;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
public interface ScheduledDateGenerator { public interface ScheduledDateGenerator {
List<LocalDate> generate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List<Holiday> holidays); List<LocalDate> generate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List<Holiday> holidays, final WorkingDays workingDays);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery,
List<LocalDate> scheduledDates); List<LocalDate> scheduledDates);
@@ -16,10 +16,14 @@ import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomain
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
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;
import org.mifosplatform.organisation.holiday.service.HolidayUtil;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException;
@@ -64,6 +68,7 @@ public class LoanScheduleAssembler {
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final ClientRepositoryWrapper clientRepository; private final ClientRepositoryWrapper clientRepository;
private final GroupRepositoryWrapper groupRepository; private final GroupRepositoryWrapper groupRepository;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
@Autowired @Autowired
public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
@@ -71,7 +76,8 @@ public class LoanScheduleAssembler {
final LoanScheduleGeneratorFactory loanScheduleFactory, final AprCalculator aprCalculator, final LoanScheduleGeneratorFactory loanScheduleFactory, final AprCalculator aprCalculator,
final LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository, final LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository,
final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService, final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService,
final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository) { final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository,
final WorkingDaysRepositoryWrapper workingDaysRepository) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository; this.loanProductRepository = loanProductRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository; this.applicationCurrencyRepository = applicationCurrencyRepository;
@@ -83,6 +89,7 @@ public class LoanScheduleAssembler {
this.configurationDomainService = configurationDomainService; this.configurationDomainService = configurationDomainService;
this.clientRepository = clientRepository; this.clientRepository = clientRepository;
this.groupRepository = groupRepository; this.groupRepository = groupRepository;
this.workingDaysRepository = workingDaysRepository;
} }
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
@@ -182,7 +189,7 @@ public class LoanScheduleAssembler {
expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment,
graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney);
} }
private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) { private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) {
if (repaymentsStartingFromDate != null if (repaymentsStartingFromDate != null
&& !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(),
@@ -221,7 +228,7 @@ public class LoanScheduleAssembler {
} }
} }
} }
public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) {
LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element);
return loanApplicationTerms.toLoanProductRelatedDetail(); return loanApplicationTerms.toLoanProductRelatedDetail();
@@ -249,11 +256,16 @@ public class LoanScheduleAssembler {
final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, expectedDisbursementDate.toDate()); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, expectedDisbursementDate.toDate());
return assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, element); final WorkingDays workingDays = this.workingDaysRepository.findOne();
validateDisbursementDateIsOnNonWorkingDay(loanApplicationTerms.getExpectedDisbursementDate(), workingDays);
validateDisbursementDateIsOnHoliday(loanApplicationTerms.getExpectedDisbursementDate(), isHolidayEnabled, holidays);
return assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element);
} }
public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled,
final List<Holiday> holidays, final JsonElement element) { final List<Holiday> holidays, final WorkingDays workingDays, final JsonElement element) {
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element); final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element);
@@ -264,7 +276,22 @@ public class LoanScheduleAssembler {
final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays, workingDays);
}
private void validateDisbursementDateIsOnNonWorkingDay(final LocalDate disbursementDate, final WorkingDays workingDays) {
if(!WorkingDaysUtil.isWorkingDay(workingDays, disbursementDate)){
final String errorMessage = "The expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage, disbursementDate);
}
}
return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays); private void validateDisbursementDateIsOnHoliday(final LocalDate disbursementDate, final boolean isHolidayEnabled, final List<Holiday> holidays) {
if (isHolidayEnabled) {
if (HolidayUtil.isHoliday(disbursementDate, holidays)) {
final String errorMessage = "The expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage, disbursementDate);
}
}
} }
} }
@@ -22,6 +22,8 @@ import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.organisation.staff.domain.StaffRepository; import org.mifosplatform.organisation.staff.domain.StaffRepository;
import org.mifosplatform.organisation.staff.exception.StaffNotFoundException; import org.mifosplatform.organisation.staff.exception.StaffNotFoundException;
import org.mifosplatform.organisation.staff.exception.StaffRoleException; import org.mifosplatform.organisation.staff.exception.StaffRoleException;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper;
import org.mifosplatform.portfolio.collateral.domain.LoanCollateral; import org.mifosplatform.portfolio.collateral.domain.LoanCollateral;
@@ -76,6 +78,7 @@ public class LoanAssembler {
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final HolidayRepository holidayRepository; private final HolidayRepository holidayRepository;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
@Autowired @Autowired
public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
@@ -84,8 +87,9 @@ public class LoanAssembler {
final StaffRepository staffRepository, final CodeValueRepositoryWrapper codeValueRepository, final StaffRepository staffRepository, final CodeValueRepositoryWrapper codeValueRepository,
final LoanScheduleAssembler loanScheduleAssembler, final LoanChargeAssembler loanChargeAssembler, final LoanScheduleAssembler loanScheduleAssembler, final LoanChargeAssembler loanChargeAssembler,
final CollateralAssembler loanCollateralAssembler, final LoanSummaryWrapper loanSummaryWrapper, final CollateralAssembler loanCollateralAssembler, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService) { final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService,
final WorkingDaysRepositoryWrapper workingDaysRepository) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository; this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository; this.clientRepository = clientRepository;
@@ -101,6 +105,7 @@ public class LoanAssembler {
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.holidayRepository = holidayRepository; this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService; this.configurationDomainService = configurationDomainService;
this.workingDaysRepository = workingDaysRepository;
} }
public Loan assembleFrom(final JsonCommand command, final AppUser currentUser) { public Loan assembleFrom(final JsonCommand command, final AppUser currentUser) {
@@ -184,10 +189,10 @@ public class LoanAssembler {
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), loanApplicationTerms.getExpectedDisbursementDate().toDate()); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), loanApplicationTerms.getExpectedDisbursementDate().toDate());
final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, element); final WorkingDays workingDays = this.workingDaysRepository.findOne();
final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element);
loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(), loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(),
submittedOnDate, externalId); submittedOnDate, externalId, isHolidayEnabled, holidays, workingDays);
return loanApplication; return loanApplication;
} }
@@ -51,7 +51,7 @@ public interface LoanWritePlatformService {
CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command); CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command);
void updateRepaymentsSchedule(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances); void applyMeetingDateChanges(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances);
void applyHolidaysToLoans(); void applyHolidaysToLoans();
} }
@@ -32,6 +32,8 @@ import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.office.domain.Office;
import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
@@ -103,6 +105,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final HolidayRepository holidayRepository; private final HolidayRepository holidayRepository;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
@Autowired @Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
@@ -115,7 +118,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository,
final PaymentDetailWritePlatformService paymentDetailWritePlatformService, final HolidayRepository holidayRepository, final PaymentDetailWritePlatformService paymentDetailWritePlatformService, final HolidayRepository holidayRepository,
final ConfigurationDomainService configurationDomainService) { final ConfigurationDomainService configurationDomainService, final WorkingDaysRepositoryWrapper workingDaysRepository) {
this.context = context; this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanAssembler = loanAssembler; this.loanAssembler = loanAssembler;
@@ -134,6 +138,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService;
this.holidayRepository = holidayRepository; this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService; this.configurationDomainService = configurationDomainService;
this.workingDaysRepository = workingDaysRepository;
} }
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
@@ -173,13 +178,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
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(), final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), actualDisbursementDate.toDate());
actualDisbursementDate.toDate()); final WorkingDays workingDays = this.workingDaysRepository.findOne();
updateLoanCounters(loan, actualDisbursementDate); updateLoanCounters(loan, actualDisbursementDate);
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate, isHolidayEnabled, holidays); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate, isHolidayEnabled, holidays, workingDays);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -262,10 +266,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// assuming repayment schedule won't regenerate because expected // assuming repayment schedule won't regenerate because expected
// disbursement and actual disbursement happens on same date // disbursement and actual disbursement happens on same date
final LocalDate firstRepaymentOnDate = null; final LocalDate firstRepaymentOnDate = null;
final boolean isHolidayEnabled = false; final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final List<Holiday> holidays = null; final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), actualDisbursementDate.toDate());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate, isHolidayEnabled, holidays); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -376,9 +383,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount); final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newRepaymentTransaction = LoanTransaction.repayment(repaymentAmount, paymentDetail, transactionDate); final LoanTransaction newRepaymentTransaction = LoanTransaction.repayment(repaymentAmount, paymentDetail, transactionDate);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate.toDate());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds); defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, isHolidayEnabled, holidays, workingDays);
this.loanTransactionRepository.save(newRepaymentTransaction); this.loanTransactionRepository.save(newRepaymentTransaction);
@@ -1011,9 +1021,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional @Transactional
@Override @Override
public void updateRepaymentsSchedule(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) { public void applyMeetingDateChanges(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final WorkingDays workingDays = this.workingDaysRepository.findOne();
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()));
@@ -1029,8 +1040,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
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, loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays);
holidays);
this.loanRepository.save(loan); this.loanRepository.save(loan);
} }
} }
@@ -1188,6 +1198,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@CronTarget(jobName = JobName.APPLY_HOLIDAYS_TO_LOANS) @CronTarget(jobName = JobName.APPLY_HOLIDAYS_TO_LOANS)
public void applyHolidaysToLoans() { public void applyHolidaysToLoans() {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
if(!isHolidayEnabled) return;
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()));
// Get all Holidays which are yet to applied to loans // Get all Holidays which are yet to applied to loans
@@ -1206,9 +1220,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
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 //FIXME: AA optimize to get all client and group loans belongs to a office id
// office id //get all group loans
// 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) {