Merge pull request #436 from ashok-conflux/MIFOSX-76

MIFOSX-76: Apply holidays to loan repayment schedule
This commit is contained in:
Keith Woodlock
2013-07-08 04:10:16 -07:00
22 changed files with 418 additions and 115 deletions
@@ -0,0 +1 @@
INSERT INTO `c_configuration` (`name`) VALUES ('reschedule-repayments-on-holidays');
@@ -10,4 +10,5 @@ public interface ConfigurationDomainService {
boolean isMakerCheckerEnabledForTask(String taskPermissionCode); boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
boolean isAmazonS3Enabled(); boolean isAmazonS3Enabled();
boolean isRescheduleFutureRepaymentsEnabled(); boolean isRescheduleFutureRepaymentsEnabled();
boolean isRescheduleRepaymentsOnHolidaysEnabled();
} }
@@ -52,5 +52,17 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
if (property == null) { throw new GlobalConfigurationPropertyNotFoundException(rescheduleRepaymentsConfigurationProperty); } if (property == null) { throw new GlobalConfigurationPropertyNotFoundException(rescheduleRepaymentsConfigurationProperty); }
return property.isEnabled(); return property.isEnabled();
} }
/* (non-Javadoc)
* @see org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService#isHolidaysEnabled()
*/
@Override
public boolean isRescheduleRepaymentsOnHolidaysEnabled() {
final String holidaysConfigurationProperty = "reschedule-repayments-on-holidays";
final GlobalConfigurationProperty property = this.globalConfigurationRepository.findOneByName(holidaysConfigurationProperty);
if (property == null) { throw new GlobalConfigurationPropertyNotFoundException(holidaysConfigurationProperty); }
return property.isEnabled();
}
} }
@@ -5,7 +5,10 @@
*/ */
package org.mifosplatform.infrastructure.core.service; package org.mifosplatform.infrastructure.core.service;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@@ -50,4 +53,10 @@ public class DateUtils {
dataValidationErrors); dataValidationErrors);
} }
} }
public static String formatToSqlDate(final Date date){
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
final String formattedSqlDate = df.format(date);
return formattedSqlDate;
}
} }
@@ -34,12 +34,10 @@ public class Holiday extends AbstractPersistable<Long> {
@Column(name = "name", unique = true, nullable = false, length = 100) @Column(name = "name", unique = true, nullable = false, length = 100)
private String name; private String name;
@SuppressWarnings("unused")
@Column(name = "from_date", nullable = false) @Column(name = "from_date", nullable = false)
@Temporal(TemporalType.DATE) @Temporal(TemporalType.DATE)
private Date fromDate; private Date fromDate;
@SuppressWarnings("unused")
@Column(name = "to_date", nullable = false) @Column(name = "to_date", nullable = false)
@Temporal(TemporalType.DATE) @Temporal(TemporalType.DATE)
private Date toDate; private Date toDate;
@@ -55,7 +53,6 @@ public class Holiday extends AbstractPersistable<Long> {
@Column(name = "description", length = 100) @Column(name = "description", length = 100)
private String description; private String description;
@SuppressWarnings("unused")
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "m_holiday_office", joinColumns = @JoinColumn(name = "holiday_id"), inverseJoinColumns = @JoinColumn(name = "office_id")) @JoinTable(name = "m_holiday_office", joinColumns = @JoinColumn(name = "holiday_id"), inverseJoinColumns = @JoinColumn(name = "office_id"))
private Set<Office> offices; private Set<Office> offices;
@@ -99,12 +96,42 @@ public class Holiday extends AbstractPersistable<Long> {
this.offices = offices; this.offices = offices;
} }
} }
protected Holiday() {}
public Date getRepaymentsRescheduledTo() { public LocalDate getRepaymentsRescheduledToLocalDate() {
return this.repaymentsRescheduledTo; LocalDate repaymentsRescheduledTo = null;
if (this.repaymentsRescheduledTo != null) {
repaymentsRescheduledTo = new LocalDate(this.repaymentsRescheduledTo);
}
return repaymentsRescheduledTo;
} }
public boolean isProcessed() { public boolean isProcessed() {
return this.processed; return this.processed;
} }
public Set<Office> getOffices() {
return this.offices;
}
public LocalDate getFromDateLocalDate() {
LocalDate fromDate = null;
if (this.fromDate != null) {
fromDate = new LocalDate(this.fromDate);
}
return fromDate;
}
public LocalDate getToDateLocalDate() {
LocalDate toDate = null;
if (this.toDate != null) {
toDate = new LocalDate(this.toDate);
}
return toDate;
}
public void processed(){
this.processed = true;
}
} }
@@ -5,11 +5,20 @@
*/ */
package org.mifosplatform.organisation.holiday.domain; package org.mifosplatform.organisation.holiday.domain;
import java.util.Date;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface HolidayRepository extends JpaRepository<Holiday, Long>, public interface HolidayRepository extends JpaRepository<Holiday, Long>,
JpaSpecificationExecutor<Holiday> { JpaSpecificationExecutor<Holiday> {
// no added behaviour @Query("select holiday from Holiday holiday, IN(holiday.offices) office where holiday.fromDate >= :fromDate and office.id = :officeId")
List<Holiday> findByOfficeIdAndGreaterThanDate(@Param("officeId") Long officeId, @Param("fromDate") Date fromDate);
@Query("from Holiday holiday where holiday.processed = false")
List<Holiday> findUnprocessed();
} }
@@ -0,0 +1,20 @@
package org.mifosplatform.organisation.holiday.service;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.holiday.domain.Holiday;
public class HolidayUtil {
public static LocalDate getRepaymentRescheduleDateToIfHoliday(final LocalDate repaymentDate, final List<Holiday> holidays) {
for (final Holiday holiday : holidays) {
if (repaymentDate.equals(holiday.getFromDateLocalDate()) || repaymentDate.equals(holiday.getToDateLocalDate())
|| (repaymentDate.isAfter(holiday.getFromDateLocalDate()) && repaymentDate.isBefore(holiday.getToDateLocalDate()))) {
// should be take from holiday
return holiday.getRepaymentsRescheduledToLocalDate();
}
}
return repaymentDate;
}
}
@@ -46,6 +46,8 @@ import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.service.HolidayUtil;
import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.data.CurrencyData;
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;
@@ -1312,8 +1314,8 @@ 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 List<Long> existingReversedTransactionIds, final Map<String, Object> actualChanges, final PaymentDetail paymentDetail,
final PaymentDetail paymentDetail, final LocalDate firstRepaymentMeetingDate) { final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, final List<Holiday> holidays) {
updateLoanToPreDisbursalState(); updateLoanToPreDisbursalState();
@@ -1339,7 +1341,7 @@ public class Loan extends AbstractPersistable<Long> {
handleDisbursementTransaction(paymentDetail, actualDisbursementDate); handleDisbursementTransaction(paymentDetail, actualDisbursementDate);
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) {
regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate); regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate, isHolidayEnabled, holidays);
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
} else { } else {
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
@@ -1366,7 +1368,8 @@ public class Loan extends AbstractPersistable<Long> {
* details/state. * details/state.
*/ */
private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate) { final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled,
final List<Holiday> holidays) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
@@ -1380,10 +1383,10 @@ public class Loan extends AbstractPersistable<Long> {
//to generate repayment schedule dates //to generate repayment schedule dates
final LocalDate firstRepaymentOnDate = (getExpectedFirstRepaymentOnDate() == null) ? firstRepaymentMeetingDate : getExpectedFirstRepaymentOnDate(); final LocalDate firstRepaymentOnDate = (getExpectedFirstRepaymentOnDate() == null) ? firstRepaymentMeetingDate : getExpectedFirstRepaymentOnDate();
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency,
loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), 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);
updateLoanSchedule(loanSchedule); updateLoanSchedule(loanSchedule);
} }
@@ -2030,7 +2033,7 @@ public class Loan extends AbstractPersistable<Long> {
return expectedDisbursementDate; return expectedDisbursementDate;
} }
private LocalDate getDisbursementDate() { public LocalDate getDisbursementDate() {
LocalDate disbursementDate = getExpectedDisbursedOnLocalDate(); LocalDate disbursementDate = getExpectedDisbursedOnLocalDate();
if (this.actualDisbursementDate != null) { if (this.actualDisbursementDate != null) {
disbursementDate = new LocalDate(this.actualDisbursementDate); disbursementDate = new LocalDate(this.actualDisbursementDate);
@@ -2345,7 +2348,7 @@ public class Loan extends AbstractPersistable<Long> {
return this.closedOnDate; return this.closedOnDate;
} }
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) {
//update repayment dates of repayment schedule installaments //update repayment dates of repayment schedule installaments
LocalDate tmpFromDate = this.getDisbursementDate();//first repayment's from date is same as disbursement date. LocalDate tmpFromDate = this.getDisbursementDate();//first repayment's from date is same as disbursement date.
LocalDate newRepaymentDate = null; LocalDate newRepaymentDate = null;
@@ -2358,6 +2361,9 @@ public class Loan extends AbstractPersistable<Long> {
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, loanRepaymentInterval, frequency); newRepaymentDate = CalendarHelper.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, loanRepaymentInterval, frequency);
if(isHolidayEnabled){
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);
@@ -2368,6 +2374,31 @@ public class Loan extends AbstractPersistable<Long> {
} }
} }
public void applyHolidayToRepaymentScheduleDates(final Holiday holiday) {
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = this.getDisbursementDate();
// Loop through all loanRepayments
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) {
final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
//update from date if it's not same as previous installament's due date.
if(!loanRepaymentScheduleInstallment.getFromDate().isEqual(tmpFromDate)){
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
if (oldDueDate.isAfter(holiday.getToDateLocalDate())) break;
if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.equals(holiday.getToDateLocalDate())
|| (oldDueDate.isAfter(holiday.getFromDateLocalDate()) && oldDueDate.isBefore(holiday.getToDateLocalDate()))) {
// should be take from holiday
final LocalDate newRepaymentDate = holiday.getRepaymentsRescheduledToLocalDate();
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
}
tmpFromDate = loanRepaymentScheduleInstallment.getDueDate();
}
}
public Group group() { public Group group() {
return this.group; return this.group;
@@ -33,29 +33,19 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
public static final String FIND_MAX_CLIENT_OR_JLG_LOAN_PRODUCT_COUNTER_QUERY = "Select MAX(l.loanProductCounter) from Loan l where " public static final String FIND_MAX_CLIENT_OR_JLG_LOAN_PRODUCT_COUNTER_QUERY = "Select MAX(l.loanProductCounter) from Loan l where "
+ "l.client.id = :clientId and l.loanProduct.id = :productId"; + "l.client.id = :clientId and l.loanProduct.id = :productId";
public static final String FIND_GROUP_LOANS_TO_UPDATE = "from Loan l where l.loanCounter > :loanCounter and " public static final String FIND_GROUP_LOANS_TO_UPDATE = "from Loan l where l.loanCounter > :loanCounter and "
+ "l.group.id = :groupId and l.loanType = :groupLoanType order by l.loanCounter"; + "l.group.id = :groupId and l.loanType = :groupLoanType order by l.loanCounter";
public static final String FIND_CLIENT_OR_JLG_LOANS_TO_UPDATE = "from Loan l where l.loanCounter > :loanCounter and " public static final String FIND_CLIENT_OR_JLG_LOANS_TO_UPDATE = "from Loan l where l.loanCounter > :loanCounter and "
+ "l.client.id = :clientId order by l.loanCounter"; + "l.client.id = :clientId order by l.loanCounter";
public static final String FIND_GROUP_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER = "from Loan l where l.loanProductCounter > :loanProductCounter" public static final String FIND_GROUP_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER = "from Loan l where l.loanProductCounter > :loanProductCounter"
+ " and l.group.id = :groupId and l.loanType = :groupLoanType and l.loanCounter is NULL order by l.loanProductCounter"; + " and l.group.id = :groupId and l.loanType = :groupLoanType and l.loanCounter is NULL order by l.loanProductCounter";
public static final String FIND_CLIENT_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER = "from Loan l where l.loanProductCounter > :loanProductCounter" public static final String FIND_CLIENT_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER = "from Loan l where l.loanProductCounter > :loanProductCounter"
+ " and l.client.id = :clientId and l.loanCounter is NULL order by l.loanProductCounter"; + " and l.client.id = :clientId and l.loanCounter is NULL order by l.loanProductCounter";
@Query("from Loan loan where loan.client.id = :clientId and loan.group.id = :groupId")
List<Loan> findByClientIdAndGroupId(@Param("clientId") Long clientId, @Param("groupId") Long groupId);
@Query("from Loan loan where loan.client.id = :clientId")
List<Loan> 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<Loan> findByIdsAndLoanStatusAndLoanType(@Param("ids") Collection<Long> ids,
@Param("loanStatuses") Collection<Integer> loanStatuses, @Param("loanTypes") Collection<Integer> loanTypes);
@Query(FIND_GROUP_LOANS_DISBURSED_AFTER) @Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") Date disbursementDate, @Param("groupId") Long groupId, List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") Date disbursementDate, @Param("groupId") Long groupId,
@Param("loanType") Integer loanType); @Param("loanType") Integer loanType);
@@ -82,11 +72,32 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
@Query(FIND_CLIENT_OR_JLG_LOANS_TO_UPDATE) @Query(FIND_CLIENT_OR_JLG_LOANS_TO_UPDATE)
List<Loan> getClientOrJLGLoansToUpdateLoanCounter(@Param("loanCounter") Integer loanCounter, @Param("clientId") Long clientId); List<Loan> getClientOrJLGLoansToUpdateLoanCounter(@Param("loanCounter") Integer loanCounter, @Param("clientId") Long clientId);
@Query(FIND_GROUP_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER) @Query(FIND_GROUP_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER)
List<Loan> getGroupLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, @Param("groupId") Long groupId, List<Loan> getGroupLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter,
@Param("groupLoanType") Integer groupLoanType); @Param("groupId") Long groupId, @Param("groupLoanType") Integer groupLoanType);
@Query(FIND_CLIENT_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER) @Query(FIND_CLIENT_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER)
List<Loan> getClientLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, @Param("clientId") Long clientId); List<Loan> getClientLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter,
@Param("clientId") Long clientId);
@Query("from Loan loan where loan.client.id = :clientId and loan.group.id = :groupId")
List<Loan> findByClientIdAndGroupId(@Param("clientId") Long clientId, @Param("groupId") Long groupId);
@Query("from Loan loan where loan.client.id = :clientId")
List<Loan> 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<Loan> findByIdsAndLoanStatusAndLoanType(@Param("ids") Collection<Long> ids,
@Param("loanStatuses") Collection<Integer> loanStatuses, @Param("loanTypes") Collection<Integer> loanTypes);
@Query("select loan.id from Loan loan where loan.actualDisbursementDate > :disbursalDate order by loan.actualDisbursementDate")
List<Long> getLoansDisbursedAfter(@Param("disbursalDate") Date disbursalDate);
@Query("from Loan loan where loan.client.office.id IN :officeIds and loan.loanStatus IN :loanStatuses")
List<Loan> findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds, @Param("loanStatuses") Collection<Integer> loanStatuses);
@Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses")
List<Loan> findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds, @Param("loanStatuses") Collection<Integer> loanStatuses);
} }
@@ -14,6 +14,7 @@ import java.util.Set;
import org.joda.time.Days; import org.joda.time.Days;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
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;
@@ -29,10 +30,10 @@ 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 LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final List<Holiday> holidays) {
// 1. generate list of proposed schedule due dates // 1. generate list of proposed schedule due dates
final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms); final List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms, isHolidayEnabled, holidays);
// 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);
@@ -9,12 +9,15 @@ import java.util.ArrayList;
import java.util.List; 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.service.HolidayUtil;
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) { public List<LocalDate> generate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled,
final List<Holiday> holidays) {
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
@@ -50,11 +53,17 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
break; break;
} }
} }
dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); //start date should be assigned before applying holidays to avoid the reset of next repayment dates.
startDate = dueRepaymentPeriodDate; startDate = dueRepaymentPeriodDate;
//Apply holidays
if(isHolidayEnabled){
dueRepaymentPeriodDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(dueRepaymentPeriodDate, holidays);
}
dueRepaymentPeriodDates.add(dueRepaymentPeriodDate);
} }
return dueRepaymentPeriodDates; return dueRepaymentPeriodDates;
} }
@@ -6,13 +6,15 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import java.math.MathContext; import java.math.MathContext;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
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); Set<LoanCharge> loanCharges, boolean isHolidayEnabled, List<Holiday> holidays);
} }
@@ -8,11 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import java.util.List; import java.util.List;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.organisation.holiday.domain.Holiday;
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); List<LocalDate> generate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List<Holiday> holidays);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery,
List<LocalDate> scheduledDates); List<LocalDate> scheduledDates);
@@ -8,10 +8,14 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService;
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.HolidayRepository;
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;
@@ -21,6 +25,10 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException;
import org.mifosplatform.portfolio.calendar.exception.MeetingFrequencyMismatchException; import org.mifosplatform.portfolio.calendar.exception.MeetingFrequencyMismatchException;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper;
import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.group.domain.GroupRepositoryWrapper;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
@@ -52,12 +60,18 @@ public class LoanScheduleAssembler {
private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final AprCalculator aprCalculator; private final AprCalculator aprCalculator;
private final CalendarRepository calendarRepository; private final CalendarRepository calendarRepository;
private final HolidayRepository holidayRepository;
private final ConfigurationDomainService configurationDomainService;
private final ClientRepositoryWrapper clientRepository;
private final GroupRepositoryWrapper groupRepository;
@Autowired @Autowired
public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
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 ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository; this.loanProductRepository = loanProductRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository; this.applicationCurrencyRepository = applicationCurrencyRepository;
@@ -65,6 +79,10 @@ public class LoanScheduleAssembler {
this.aprCalculator = aprCalculator; this.aprCalculator = aprCalculator;
this.loanChargeAssembler = loanChargeAssembler; this.loanChargeAssembler = loanChargeAssembler;
this.calendarRepository = calendarRepository; this.calendarRepository = calendarRepository;
this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
} }
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
@@ -119,32 +137,33 @@ public class LoanScheduleAssembler {
final Boolean synchDisbursement = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final Boolean synchDisbursement = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element);
final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element); final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element);
Calendar calendar = null; Calendar calendar = null;
if ((synchDisbursement != null && synchDisbursement.booleanValue()) || if ((synchDisbursement != null && synchDisbursement.booleanValue()) || (calendarId != null && calendarId != 0)) {
(calendarId != null && calendarId != 0)) { calendar = this.calendarRepository.findOne(calendarId);
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); } if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
//validate repayment frequency and interval with meeting frequency and interval // validate repayment frequency and interval with meeting frequency and interval
PeriodFrequencyType meetingPeriodFrequency = CalendarHelper.getMeetingPeriodFrequencyType(calendar.getRecurrence()); PeriodFrequencyType meetingPeriodFrequency = CalendarHelper.getMeetingPeriodFrequencyType(calendar.getRecurrence());
validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType, validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType,
CalendarHelper.getInterval(calendar.getRecurrence()),repaymentEvery); CalendarHelper.getInterval(calendar.getRecurrence()), repaymentEvery);
} }
if (synchDisbursement != null && synchDisbursement.booleanValue()) { if (synchDisbursement != null && synchDisbursement.booleanValue()) {
validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar);
} }
//if calendar is attached (not null) then reschedule repayment dates according to meeting // if calendar is attached (not null) then reschedule repayment dates according to meeting
if (null != calendar) { if (null != calendar) {
//TODO : AA - Is it require to reset repaymentsStartingFromDate or set only if it is not provided (or null) // TODO : AA - Is it require to reset repaymentsStartingFromDate or
// Currently provided repaymentsStartingFromDate takes precedence over system generated next meeting date // set only if it is not provided (or null)
if(repaymentsStartingFromDate == null){ // Currently provided repaymentsStartingFromDate takes precedence over system generated next meeting date
//FIXME: AA - Possibility of having next meeting date immediately after disbursement date, if (repaymentsStartingFromDate == null) {
//need to have minimum number of days gap between disbursement and first repayment date. // FIXME: AA - Possibility of having next meeting date immediately after disbursement date,
final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); // need to have minimum number of days gap between disbursement and first repayment date.
repaymentsStartingFromDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate, repaymentEvery, frequency); final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
}else{//validate user provided repaymentsStartFromDate repaymentsStartingFromDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate,
validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar); repaymentEvery, frequency);
} else {// validate user provided repaymentsStartFromDate
validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar);
} }
} }
// grace details // grace details
@@ -164,59 +183,77 @@ public class LoanScheduleAssembler {
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 && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repaymentsStartingFromDate)){ if (repaymentsStartingFromDate != null
final String errorMessage = "First repayment date '" + repaymentsStartingFromDate + "' do not fall on a meeting date"; && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(),
throw new LoanApplicationDateException("first.repayment.date.do.not.match.meeting.date", errorMessage, repaymentsStartingFromDate); repaymentsStartingFromDate)) {
} final String errorMessage = "First repayment date '" + repaymentsStartingFromDate + "' do not fall on a meeting date";
} throw new LoanApplicationDateException("first.repayment.date.do.not.match.meeting.date", errorMessage,
repaymentsStartingFromDate);
}
}
private void validateDisbursementDateWithMeetingDates( private void validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate, final Calendar calendar) {
final LocalDate expectedDisbursementDate, final Calendar calendar) { // disbursement date should fall on a meeting date
// disbursement date should fall on a meeting date if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), expectedDisbursementDate)) {
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), final String errorMessage = "Expected disbursement date '" + expectedDisbursementDate + "' do not fall on a meeting date";
calendar.getStartDateLocalDate(), expectedDisbursementDate)) { throw new LoanApplicationDateException("disbursement.date.do.not.match.meeting.date", errorMessage, expectedDisbursementDate);
final String errorMessage = "Expected disbursement date '" }
+ expectedDisbursementDate
+ "' do not fall on a meeting date";
throw new LoanApplicationDateException(
"disbursement.date.do.not.match.meeting.date",
errorMessage, expectedDisbursementDate);
}
} }
private void validateRepaymentFrequencyIsSameAsMeetingFrequency( private void validateRepaymentFrequencyIsSameAsMeetingFrequency(final Integer meetingFrequency, final Integer repaymentFrequency,
final Integer meetingFrequency, final Integer repaymentFrequency, final Integer meetingInterval, final Integer repaymentInterval) {
final Integer meetingInterval, final Integer repaymentInterval) { // meeting with daily frequency should allow loan products with any
//meeting with daily frequency should allow loan products with any frequency. // frequency.
if(!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)){ if (!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)) {
//repayment frequency must match with meeting frequency // repayment frequency must match with meeting frequency
if(!meetingFrequency.equals(repaymentFrequency)){ if (!meetingFrequency.equals(repaymentFrequency)) {
throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency", "Loan repayment frequency period must match that of meeting frequency period", repaymentFrequency); throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency",
}else if(meetingFrequency.equals(repaymentFrequency)){//repayment frequency is same as meeting frequency "Loan repayment frequency period must match that of meeting frequency period", repaymentFrequency);
//repayment interval should be same or multiple of meeting interval } else if (meetingFrequency.equals(repaymentFrequency)) {
if (repaymentInterval % meetingInterval != 0) { // repayment frequency is same as meeting frequency repayment interval should be same or multiple of meeting interval
// throw exception: Loan product frequency/interval if (repaymentInterval % meetingInterval != 0) {
throw new MeetingFrequencyMismatchException("loanapplication.repayment.interval", "Loan repayment repaid every # must equal or multiple of meeting interval", repaymentInterval); // throw exception: Loan product frequency/interval
} throw new MeetingFrequencyMismatchException("loanapplication.repayment.interval",
} "Loan repayment repaid every # must equal or multiple of meeting interval", repaymentInterval);
} }
} }
}
}
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();
} }
public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) { public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) {
//This method is getting called from calculate loan schedule.
final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element);
//Get holiday details
return assembleLoanScheduleFrom(loanApplicationTerms, element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final Long clientId = fromApiJsonHelper.extractLongNamed("clientId", element);
final Long groupId = fromApiJsonHelper.extractLongNamed("groupId", element);
Client client = null;
Group group = null;
Long officeId = null;
if(clientId != null){
client = this.clientRepository.findOneWithNotFoundDetection(clientId);
officeId = client.getOffice().getId();
}else if (groupId != null){
group = this.groupRepository.findOneWithNotFoundDetection(groupId);
officeId = group.getOffice().getId();
}
final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, expectedDisbursementDate.toDate());
return assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, element);
} }
public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final JsonElement element) { public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled,
final List<Holiday> holidays, final JsonElement element) {
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element); final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element);
@@ -228,6 +265,6 @@ 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); return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays);
} }
} }
@@ -159,7 +159,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(newLoanApplication.getTermFrequency(), newLoanApplication.getTermPeriodFrequencyType(), this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(newLoanApplication.getTermFrequency(), newLoanApplication.getTermPeriodFrequencyType(),
productRelatedDetail.getNumberOfRepayments(), productRelatedDetail.getRepayEvery(), productRelatedDetail.getNumberOfRepayments(), productRelatedDetail.getRepayEvery(),
productRelatedDetail.getRepaymentPeriodFrequencyType().getValue()); productRelatedDetail.getRepaymentPeriodFrequencyType().getValue());
this.loanRepository.save(newLoanApplication); this.loanRepository.save(newLoanApplication);
if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) { if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) {
@@ -12,9 +12,12 @@ import java.util.Set;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService;
import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.data.EnumOptionData;
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.HolidayRepository;
import org.mifosplatform.organisation.staff.domain.Staff; 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;
@@ -71,6 +74,8 @@ public class LoanAssembler {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final LoanSummaryWrapper loanSummaryWrapper; private final LoanSummaryWrapper loanSummaryWrapper;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final HolidayRepository holidayRepository;
private final ConfigurationDomainService configurationDomainService;
@Autowired @Autowired
public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository,
@@ -79,7 +84,8 @@ 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) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository; this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository; this.clientRepository = clientRepository;
@@ -93,6 +99,8 @@ public class LoanAssembler {
this.loanCollateralAssembler = loanCollateralAssembler; this.loanCollateralAssembler = loanCollateralAssembler;
this.loanSummaryWrapper = loanSummaryWrapper; this.loanSummaryWrapper = loanSummaryWrapper;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService;
} }
public Loan assembleFrom(final JsonCommand command, final AppUser currentUser) { public Loan assembleFrom(final JsonCommand command, final AppUser currentUser) {
@@ -174,7 +182,9 @@ public class LoanAssembler {
this.loanRepaymentScheduleTransactionProcessorFactory); this.loanRepaymentScheduleTransactionProcessorFactory);
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element);
final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), loanApplicationTerms.getExpectedDisbursementDate().toDate());
final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, element);
loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(), loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(),
submittedOnDate, externalId); submittedOnDate, externalId);
@@ -52,4 +52,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command); CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command);
void updateRepaymentsSchedule(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances); void updateRepaymentsSchedule(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances);
void applyHolidaysToLoans();
} }
@@ -12,18 +12,23 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService; import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService;
import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.domain.HolidayRepository;
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.office.domain.Office;
import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.organisation.staff.domain.Staff;
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;
@@ -94,6 +99,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final CalendarInstanceRepository calendarInstanceRepository; private final CalendarInstanceRepository calendarInstanceRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final HolidayRepository holidayRepository;
private final ConfigurationDomainService configurationDomainService;
@Autowired @Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
@@ -105,7 +112,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper, final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository,
final PaymentDetailWritePlatformService paymentDetailWritePlatformService) { final PaymentDetailWritePlatformService paymentDetailWritePlatformService,
final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService) {
this.context = context; this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanAssembler = loanAssembler; this.loanAssembler = loanAssembler;
@@ -122,6 +130,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanScheduleFactory = loanScheduleFactory; this.loanScheduleFactory = loanScheduleFactory;
this.calendarInstanceRepository = calendarInstanceRepository; this.calendarInstanceRepository = calendarInstanceRepository;
this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService;
this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService;
} }
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
@@ -160,11 +170,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
//Recalculate first repayment date based in actual disbursement date. //Recalculate first repayment date based in actual disbursement date.
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance); final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), actualDisbursementDate.toDate());
updateLoanCounters(loan, actualDisbursementDate); updateLoanCounters(loan, actualDisbursementDate);
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate, isHolidayEnabled, holidays);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -250,8 +262,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
//FIXME: AA - this should be first meeting date based on disbursement date and next available meeting dates //FIXME: AA - this should be first meeting date based on disbursement date and next available meeting dates
//assuming repayment schedule won't regenerate because expected disbursement and actual disbursement happens on same date //assuming repayment schedule won't regenerate because expected disbursement and actual disbursement happens on same date
final LocalDate firstRepaymentOnDate = null; final LocalDate firstRepaymentOnDate = null;
final boolean isHolidayEnabled = false;
final List<Holiday> holidays = null;
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate, isHolidayEnabled, holidays);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -990,9 +1004,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
} }
@Transactional
@Override @Override
public void updateRepaymentsSchedule(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) { public void updateRepaymentsSchedule(final Calendar calendar, Collection<CalendarInstance> loanCalendarInstances) {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final 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()));
@@ -1003,10 +1019,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
final List<Loan> loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes); final List<Loan> loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes);
List<Holiday> holidays = null;
//loop through each loan to reschedule the repayment dates //loop through each loan to reschedule the repayment dates
for (Loan loan : loans) { for (Loan loan : loans) {
if(loan != null){ if(loan != null){
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence()); holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, holidays);
this.loanRepository.save(loan); this.loanRepository.save(loan);
} }
} }
@@ -1157,4 +1175,40 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
this.loanRepository.save(loansToUpdate); this.loanRepository.save(loansToUpdate);
} }
@Transactional
@Override
public void applyHolidaysToLoans() {
final Collection<Integer> loanStatuses = new ArrayList<Integer>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
// Get all Holidays which are yet to applied to loans
List<Holiday> holidays = this.holidayRepository.findUnprocessed();
// Loop through all holidays
for (Holiday holiday : holidays) {
// All offices to which holiday is applied
final Set<Office> offices = holiday.getOffices();
Collection<Long> officeIds = new ArrayList<Long>(offices.size());
for (Office office : offices) {
officeIds.add(office.getId());
}
// get all loans
List<Loan> loans = new ArrayList<Loan>();
//get all individual and jlg loans
loans.addAll(this.loanRepository.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses));
//FIXME: optimize to get all client and group loans belongs to a office id
//get all group loans
loans.addAll(this.loanRepository.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (Loan loan : loans) {
// apply holiday
loan.applyHolidayToRepaymentScheduleDates(holiday);
}
this.loanRepository.save(loans);
holiday.processed();
}
this.holidayRepository.save(holidays);
}
} }
@@ -0,0 +1,31 @@
/**
* 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.scheduledjobs;
import java.util.Date;
import org.mifosplatform.scheduledjobs.service.ScheduledJobRunnerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApplyHolidaysToLoansScheduledJob {
private final static Logger logger = LoggerFactory.getLogger(ApplyHolidaysToLoansScheduledJob.class);
private final ScheduledJobRunnerService scheduledJobRunnerService;
public ApplyHolidaysToLoansScheduledJob(final ScheduledJobRunnerService scheduledJobRunnerService) {
this.scheduledJobRunnerService = scheduledJobRunnerService;
}
public void execute() {
logger.info("Running Job ApplyHolidaysToLoansScheduledJob " + new Date());
this.scheduledJobRunnerService.applyHolidaysToLoans();
logger.info("Finishing Job ApplyHolidaysToLoansScheduledJob " + new Date());
}
}
@@ -14,4 +14,6 @@ public interface ScheduledJobRunnerService {
void updateLoanPaidInAdvance(); void updateLoanPaidInAdvance();
void applyAnnualFeeForSavings(); void applyAnnualFeeForSavings();
void applyHolidaysToLoans();
} }
@@ -11,6 +11,7 @@ import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant;
import org.mifosplatform.infrastructure.core.service.DataSourcePerTenantService; import org.mifosplatform.infrastructure.core.service.DataSourcePerTenantService;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.security.service.TenantDetailsService; import org.mifosplatform.infrastructure.security.service.TenantDetailsService;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -27,13 +28,16 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
private final TenantDetailsService tenantDetailsService; private final TenantDetailsService tenantDetailsService;
private final DataSourcePerTenantService dataSourcePerTenantService; private final DataSourcePerTenantService dataSourcePerTenantService;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
private final LoanWritePlatformService loanWritePlatformService;
@Autowired @Autowired
public ScheduledJobRunnerServiceImpl(final DataSourcePerTenantService dataSourcePerTenantService, public ScheduledJobRunnerServiceImpl(final DataSourcePerTenantService dataSourcePerTenantService,
final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService,
final LoanWritePlatformService loanWritePlatformService) {
this.dataSourcePerTenantService = dataSourcePerTenantService; this.dataSourcePerTenantService = dataSourcePerTenantService;
this.tenantDetailsService = tenantDetailsService; this.tenantDetailsService = tenantDetailsService;
this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService;
this.loanWritePlatformService = loanWritePlatformService;
} }
@Transactional @Transactional
@@ -220,4 +224,14 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
logger.info(tenant.getName() + ": Results affected by update: " + savingsIds.size()); logger.info(tenant.getName() + ": Results affected by update: " + savingsIds.size());
} }
} }
@Transactional
@Override
public void applyHolidaysToLoans() {
List<MifosPlatformTenant> allTenants = this.tenantDetailsService.findAllTenants();
for (MifosPlatformTenant tenant : allTenants) {
ThreadLocalContextUtil.setTenant(tenant);
this.loanWritePlatformService.applyHolidaysToLoans();
}
}
} }
@@ -26,7 +26,11 @@
class="org.mifosplatform.scheduledjobs.ApplyAnnualFeeForSavingsScheduledJob"> class="org.mifosplatform.scheduledjobs.ApplyAnnualFeeForSavingsScheduledJob">
<constructor-arg ref="scheduledJobRunnerService" /> <constructor-arg ref="scheduledJobRunnerService" />
</bean> </bean>
<bean id="applyHolidaysToLoansScheduledJob"
class="org.mifosplatform.scheduledjobs.ApplyHolidaysToLoansScheduledJob">
<constructor-arg ref="scheduledJobRunnerService" />
</bean>
<bean id="updateLoanSummariesJobDetail" <bean id="updateLoanSummariesJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="group" value="mifosJobs" /> <property name="group" value="mifosJobs" />
@@ -58,6 +62,14 @@
<property name="targetMethod" value="execute" /> <property name="targetMethod" value="execute" />
<property name="concurrent" value="false" /> <property name="concurrent" value="false" />
</bean> </bean>
<bean id="applyHolidaysToLoansJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="group" value="mifosJobs" />
<property name="targetObject" ref="applyHolidaysToLoansScheduledJob" />
<property name="targetMethod" value="execute" />
<property name="concurrent" value="false" />
</bean>
<bean id="updateLoanSummariesJobCronTrigger" <bean id="updateLoanSummariesJobCronTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
@@ -83,6 +95,12 @@
<property name="cronExpression" value="0 20 22 1/1 * ? *" /> <property name="cronExpression" value="0 20 22 1/1 * ? *" />
</bean> </bean>
<bean id="applyHolidaysToJobDetailCronTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="applyHolidaysToLoansJobDetail" />
<property name="cronExpression" value="0 0 12 * * ?" />
</bean>
<bean name="schedulerFactory" <bean name="schedulerFactory"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers"> <property name="triggers">
@@ -91,6 +109,7 @@
<ref bean="updateLoanArrearsAgeingJobDetailCronTrigger" /> <ref bean="updateLoanArrearsAgeingJobDetailCronTrigger" />
<ref bean="updateLoanPaidInAdvanceJobDetailCronTrigger" /> <ref bean="updateLoanPaidInAdvanceJobDetailCronTrigger" />
<ref bean="applyAnnualFeeForSavingsJobDetailCronTrigger" /> <ref bean="applyAnnualFeeForSavingsJobDetailCronTrigger" />
<ref bean="applyHolidaysToJobDetailCronTrigger" />
</list> </list>
</property> </property>
</bean> </bean>