diff --git a/mifosng-db/migrations/core_db/V75__add_reschedule-repayments-on-holidays_to_configuration.sql b/mifosng-db/migrations/core_db/V75__add_reschedule-repayments-on-holidays_to_configuration.sql new file mode 100644 index 000000000..1325e3676 --- /dev/null +++ b/mifosng-db/migrations/core_db/V75__add_reschedule-repayments-on-holidays_to_configuration.sql @@ -0,0 +1 @@ +INSERT INTO `c_configuration` (`name`) VALUES ('reschedule-repayments-on-holidays'); \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java index 45d8bc59c..7bd6f8cf6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -10,4 +10,5 @@ public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); boolean isAmazonS3Enabled(); boolean isRescheduleFutureRepaymentsEnabled(); + boolean isRescheduleRepaymentsOnHolidaysEnabled(); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index f9d861b1d..8adcffe12 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -52,5 +52,17 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService if (property == null) { throw new GlobalConfigurationPropertyNotFoundException(rescheduleRepaymentsConfigurationProperty); } 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(); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DateUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DateUtils.java index dd7fc4a52..19313290d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DateUtils.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/DateUtils.java @@ -5,7 +5,10 @@ */ package org.mifosplatform.infrastructure.core.service; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.joda.time.DateTime; @@ -50,4 +53,10 @@ public class DateUtils { dataValidationErrors); } } + + public static String formatToSqlDate(final Date date){ + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + final String formattedSqlDate = df.format(date); + return formattedSqlDate; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/Holiday.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/Holiday.java index e620e40f8..3fd2487e4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/Holiday.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/Holiday.java @@ -34,12 +34,10 @@ public class Holiday extends AbstractPersistable { @Column(name = "name", unique = true, nullable = false, length = 100) private String name; - @SuppressWarnings("unused") @Column(name = "from_date", nullable = false) @Temporal(TemporalType.DATE) private Date fromDate; - @SuppressWarnings("unused") @Column(name = "to_date", nullable = false) @Temporal(TemporalType.DATE) private Date toDate; @@ -55,7 +53,6 @@ public class Holiday extends AbstractPersistable { @Column(name = "description", length = 100) private String description; - @SuppressWarnings("unused") @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "m_holiday_office", joinColumns = @JoinColumn(name = "holiday_id"), inverseJoinColumns = @JoinColumn(name = "office_id")) private Set offices; @@ -99,12 +96,42 @@ public class Holiday extends AbstractPersistable { this.offices = offices; } } + + protected Holiday() {} - public Date getRepaymentsRescheduledTo() { - return this.repaymentsRescheduledTo; + public LocalDate getRepaymentsRescheduledToLocalDate() { + LocalDate repaymentsRescheduledTo = null; + if (this.repaymentsRescheduledTo != null) { + repaymentsRescheduledTo = new LocalDate(this.repaymentsRescheduledTo); + } + return repaymentsRescheduledTo; } public boolean isProcessed() { return this.processed; } + + public Set 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; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/HolidayRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/HolidayRepository.java index 985351c1c..478b0cc53 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/HolidayRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/domain/HolidayRepository.java @@ -5,11 +5,20 @@ */ 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.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface HolidayRepository extends JpaRepository, JpaSpecificationExecutor { - // no added behaviour + @Query("select holiday from Holiday holiday, IN(holiday.offices) office where holiday.fromDate >= :fromDate and office.id = :officeId") + List findByOfficeIdAndGreaterThanDate(@Param("officeId") Long officeId, @Param("fromDate") Date fromDate); + + @Query("from Holiday holiday where holiday.processed = false") + List findUnprocessed(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/service/HolidayUtil.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/service/HolidayUtil.java new file mode 100644 index 000000000..47c366811 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/holiday/service/HolidayUtil.java @@ -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 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; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index deee65bc9..5362973de 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -46,6 +46,8 @@ import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.service.DateUtils; 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.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -1312,8 +1314,8 @@ public class Loan extends AbstractPersistable { public Map disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, - final List existingReversedTransactionIds, final Map actualChanges, - final PaymentDetail paymentDetail, final LocalDate firstRepaymentMeetingDate) { + final List existingReversedTransactionIds, final Map actualChanges, final PaymentDetail paymentDetail, + final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, final List holidays) { updateLoanToPreDisbursalState(); @@ -1339,7 +1341,7 @@ public class Loan extends AbstractPersistable { handleDisbursementTransaction(paymentDetail, actualDisbursementDate); if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { - regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate); + regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate, isHolidayEnabled, holidays); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); } else { updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); @@ -1366,7 +1368,8 @@ public class Loan extends AbstractPersistable { * details/state. */ private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, - final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate) { + final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate, final boolean isHolidayEnabled, + final List holidays) { final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); @@ -1380,10 +1383,10 @@ public class Loan extends AbstractPersistable { //to generate repayment schedule dates final LocalDate firstRepaymentOnDate = (getExpectedFirstRepaymentOnDate() == null) ? firstRepaymentMeetingDate : getExpectedFirstRepaymentOnDate(); LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, - loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), firstRepaymentOnDate, getInArrearsTolerance(), + loanTermPeriodFrequencyType, getDisbursementDate(), firstRepaymentOnDate, getInArrearsTolerance(), this.loanRepaymentScheduleDetail); - LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges); + LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges, isHolidayEnabled, holidays); updateLoanSchedule(loanSchedule); } @@ -2030,7 +2033,7 @@ public class Loan extends AbstractPersistable { return expectedDisbursementDate; } - private LocalDate getDisbursementDate() { + public LocalDate getDisbursementDate() { LocalDate disbursementDate = getExpectedDisbursedOnLocalDate(); if (this.actualDisbursementDate != null) { disbursementDate = new LocalDate(this.actualDisbursementDate); @@ -2345,7 +2348,7 @@ public class Loan extends AbstractPersistable { 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 holidays) { //update repayment dates of repayment schedule installaments LocalDate tmpFromDate = this.getDisbursementDate();//first repayment's from date is same as disbursement date. LocalDate newRepaymentDate = null; @@ -2358,6 +2361,9 @@ public class Loan extends AbstractPersistable { final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); // reset repayment date with new meeting date newRepaymentDate = CalendarHelper.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, loanRepaymentInterval, frequency); + if(isHolidayEnabled){ + newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); + } loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); //reset from date to get actual daysInPeriod loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); @@ -2368,6 +2374,31 @@ public class Loan extends AbstractPersistable { } } + + + + 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() { return this.group; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java index cf0d38fc6..955d6a210 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java @@ -33,29 +33,19 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat 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"; - + 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"; - + 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"; - + 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"; - + 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"; - @Query("from Loan loan where loan.client.id = :clientId and loan.group.id = :groupId") - List findByClientIdAndGroupId(@Param("clientId") Long clientId, @Param("groupId") Long groupId); - - @Query("from Loan loan where loan.client.id = :clientId") - List findLoanByClientId(@Param("clientId") Long clientId); - - @Query("from Loan loan where loan.id IN :ids and loan.loanStatus IN :loanStatuses and loan.loanType IN :loanTypes") - List findByIdsAndLoanStatusAndLoanType(@Param("ids") Collection ids, - @Param("loanStatuses") Collection loanStatuses, @Param("loanTypes") Collection loanTypes); - @Query(FIND_GROUP_LOANS_DISBURSED_AFTER) List getGroupLoansDisbursedAfter(@Param("disbursementDate") Date disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -82,11 +72,32 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_CLIENT_OR_JLG_LOANS_TO_UPDATE) List getClientOrJLGLoansToUpdateLoanCounter(@Param("loanCounter") Integer loanCounter, @Param("clientId") Long clientId); - + @Query(FIND_GROUP_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER) - List getGroupLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, @Param("groupId") Long groupId, - @Param("groupLoanType") Integer groupLoanType); - + List getGroupLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, + @Param("groupId") Long groupId, @Param("groupLoanType") Integer groupLoanType); + @Query(FIND_CLIENT_LOANS_TO_UPDATE_LOANPRODUCT_COUNTER) - List getClientLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, @Param("clientId") Long clientId); + List getClientLoansToUpdateLoanProductCounter(@Param("loanProductCounter") Integer loanProductCounter, + @Param("clientId") Long clientId); + + @Query("from Loan loan where loan.client.id = :clientId and loan.group.id = :groupId") + List findByClientIdAndGroupId(@Param("clientId") Long clientId, @Param("groupId") Long groupId); + + @Query("from Loan loan where loan.client.id = :clientId") + List findLoanByClientId(@Param("clientId") Long clientId); + + @Query("from Loan loan where loan.id IN :ids and loan.loanStatus IN :loanStatuses and loan.loanType IN :loanTypes") + List findByIdsAndLoanStatusAndLoanType(@Param("ids") Collection ids, + @Param("loanStatuses") Collection loanStatuses, @Param("loanTypes") Collection loanTypes); + + @Query("select loan.id from Loan loan where loan.actualDisbursementDate > :disbursalDate order by loan.actualDisbursementDate") + List getLoansDisbursedAfter(@Param("disbursalDate") Date disbursalDate); + + @Query("from Loan loan where loan.client.office.id IN :officeIds and loan.loanStatus IN :loanStatuses") + List findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, @Param("loanStatuses") Collection loanStatuses); + + @Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses") + List findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, @Param("loanStatuses") Collection loanStatuses); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 8a342c5cd..de3756349 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -14,6 +14,7 @@ import java.util.Set; import org.joda.time.Days; 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.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; @@ -29,10 +30,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener @Override public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency, - final LoanApplicationTerms loanApplicationTerms, final Set loanCharges) { + final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final boolean isHolidayEnabled, final List holidays) { // 1. generate list of proposed schedule due dates - final List scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms); + final List scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms, isHolidayEnabled, holidays); // 2. determine the total charges due at time of disbursement final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 575ee2c00..5681a843a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -9,12 +9,15 @@ import java.util.ArrayList; import java.util.List; 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; public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override - public List generate(final LoanApplicationTerms loanApplicationTerms) { + public List generate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, + final List holidays) { final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); @@ -50,11 +53,17 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { break; } } - - dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); + + //start date should be assigned before applying holidays to avoid the reset of next repayment dates. startDate = dueRepaymentPeriodDate; + //Apply holidays + if(isHolidayEnabled){ + dueRepaymentPeriodDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(dueRepaymentPeriodDate, holidays); + } + + dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); } - + return dueRepaymentPeriodDates; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 721fad5fb..2457b7f4e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -6,13 +6,15 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.math.MathContext; +import java.util.List; import java.util.Set; +import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; public interface LoanScheduleGenerator { LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms, - Set loanCharges); + Set loanCharges, boolean isHolidayEnabled, List holidays); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index ba8722e26..e2a8756b3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -8,11 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.util.List; import org.joda.time.LocalDate; +import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface ScheduledDateGenerator { - List generate(LoanApplicationTerms loanApplicationTerms); + List generate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List holidays); LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, List scheduledDates); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 2b4fac643..179e7d84d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -8,10 +8,14 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import java.util.List; import java.util.Set; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; 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.ApplicationCurrencyRepositoryWrapper; 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.MeetingFrequencyMismatchException; 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.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; @@ -52,12 +60,18 @@ public class LoanScheduleAssembler { private final LoanScheduleGeneratorFactory loanScheduleFactory; private final AprCalculator aprCalculator; private final CalendarRepository calendarRepository; + private final HolidayRepository holidayRepository; + private final ConfigurationDomainService configurationDomainService; + private final ClientRepositoryWrapper clientRepository; + private final GroupRepositoryWrapper groupRepository; @Autowired public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, 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.loanProductRepository = loanProductRepository; this.applicationCurrencyRepository = applicationCurrencyRepository; @@ -65,6 +79,10 @@ public class LoanScheduleAssembler { this.aprCalculator = aprCalculator; this.loanChargeAssembler = loanChargeAssembler; this.calendarRepository = calendarRepository; + this.holidayRepository = holidayRepository; + this.configurationDomainService = configurationDomainService; + this.clientRepository = clientRepository; + this.groupRepository = groupRepository; } public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { @@ -119,32 +137,33 @@ public class LoanScheduleAssembler { final Boolean synchDisbursement = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element); Calendar calendar = null; - if ((synchDisbursement != null && synchDisbursement.booleanValue()) || - (calendarId != null && calendarId != 0)) { - calendar = this.calendarRepository.findOne(calendarId); + if ((synchDisbursement != null && synchDisbursement.booleanValue()) || (calendarId != null && calendarId != 0)) { + calendar = this.calendarRepository.findOne(calendarId); if (calendar == null) { throw new CalendarNotFoundException(calendarId); } - - //validate repayment frequency and interval with meeting frequency and interval - PeriodFrequencyType meetingPeriodFrequency = CalendarHelper.getMeetingPeriodFrequencyType(calendar.getRecurrence()); - validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType, - CalendarHelper.getInterval(calendar.getRecurrence()),repaymentEvery); + + // validate repayment frequency and interval with meeting frequency and interval + PeriodFrequencyType meetingPeriodFrequency = CalendarHelper.getMeetingPeriodFrequencyType(calendar.getRecurrence()); + validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType, + CalendarHelper.getInterval(calendar.getRecurrence()), repaymentEvery); } - if (synchDisbursement != null && synchDisbursement.booleanValue()) { - validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); - } - - //if calendar is attached (not null) then reschedule repayment dates according to meeting + if (synchDisbursement != null && synchDisbursement.booleanValue()) { + validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); + } + + // if calendar is attached (not null) then reschedule repayment dates according to meeting if (null != calendar) { - //TODO : AA - Is it require to reset repaymentsStartingFromDate or set only if it is not provided (or null) - // Currently provided repaymentsStartingFromDate takes precedence over system generated next meeting date - if(repaymentsStartingFromDate == null){ - //FIXME: AA - Possibility of having next meeting date immediately after disbursement date, - //need to have minimum number of days gap between disbursement and first repayment date. - final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); - repaymentsStartingFromDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate, repaymentEvery, frequency); - }else{//validate user provided repaymentsStartFromDate - validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar); + // TODO : AA - Is it require to reset repaymentsStartingFromDate or + // set only if it is not provided (or null) + // Currently provided repaymentsStartingFromDate takes precedence over system generated next meeting date + if (repaymentsStartingFromDate == null) { + // FIXME: AA - Possibility of having next meeting date immediately after disbursement date, + // need to have minimum number of days gap between disbursement and first repayment date. + final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + repaymentsStartingFromDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate, + repaymentEvery, frequency); + } else {// validate user provided repaymentsStartFromDate + validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar); } } // grace details @@ -164,59 +183,77 @@ public class LoanScheduleAssembler { graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); } - private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) { - if(repaymentsStartingFromDate != null && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), 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 validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) { + if (repaymentsStartingFromDate != null + && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), + 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( - final LocalDate expectedDisbursementDate, final Calendar calendar) { - // disbursement date should fall on a meeting date - if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), - calendar.getStartDateLocalDate(), 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 validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate, final Calendar calendar) { + // disbursement date should fall on a meeting date + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), 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( - final Integer meetingFrequency, final Integer repaymentFrequency, - final Integer meetingInterval, final Integer repaymentInterval) { - //meeting with daily frequency should allow loan products with any frequency. - if(!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)){ - //repayment frequency must match with meeting frequency - if(!meetingFrequency.equals(repaymentFrequency)){ - throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency", "Loan repayment frequency period must match that of meeting frequency period", repaymentFrequency); - }else if(meetingFrequency.equals(repaymentFrequency)){//repayment frequency is same as meeting frequency - //repayment interval should be same or multiple of meeting interval - if (repaymentInterval % meetingInterval != 0) { - // throw exception: Loan product frequency/interval - throw new MeetingFrequencyMismatchException("loanapplication.repayment.interval", "Loan repayment repaid every # must equal or multiple of meeting interval", repaymentInterval); - } - } - } - } + private void validateRepaymentFrequencyIsSameAsMeetingFrequency(final Integer meetingFrequency, final Integer repaymentFrequency, + final Integer meetingInterval, final Integer repaymentInterval) { + // meeting with daily frequency should allow loan products with any + // frequency. + if (!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)) { + // repayment frequency must match with meeting frequency + if (!meetingFrequency.equals(repaymentFrequency)) { + throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency", + "Loan repayment frequency period must match that of meeting frequency period", repaymentFrequency); + } else if (meetingFrequency.equals(repaymentFrequency)) { + // repayment frequency is same as meeting frequency repayment interval should be same or multiple of meeting interval + if (repaymentInterval % meetingInterval != 0) { + // 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); return loanApplicationTerms.toLoanProductRelatedDetail(); } public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) { - + //This method is getting called from calculate loan schedule. final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); - - return assembleLoanScheduleFrom(loanApplicationTerms, element); + //Get holiday details + 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 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 holidays, final JsonElement element) { final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element); @@ -228,6 +265,6 @@ public class LoanScheduleAssembler { final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges); + return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 07253b254..d07f0fa34 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -159,7 +159,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(newLoanApplication.getTermFrequency(), newLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(), productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue()); - + this.loanRepository.save(newLoanApplication); if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index 221be5b74..ed106833b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -12,9 +12,12 @@ import java.util.Set; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.codes.domain.CodeValue; 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.data.EnumOptionData; 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.StaffRepository; import org.mifosplatform.organisation.staff.exception.StaffNotFoundException; @@ -71,6 +74,8 @@ public class LoanAssembler { private final FromJsonHelper fromApiJsonHelper; private final LoanSummaryWrapper loanSummaryWrapper; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final HolidayRepository holidayRepository; + private final ConfigurationDomainService configurationDomainService; @Autowired public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, @@ -79,7 +84,8 @@ public class LoanAssembler { final StaffRepository staffRepository, final CodeValueRepositoryWrapper codeValueRepository, final LoanScheduleAssembler loanScheduleAssembler, final LoanChargeAssembler loanChargeAssembler, final CollateralAssembler loanCollateralAssembler, final LoanSummaryWrapper loanSummaryWrapper, - final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory) { + final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, + final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService) { this.fromApiJsonHelper = fromApiJsonHelper; this.loanProductRepository = loanProductRepository; this.clientRepository = clientRepository; @@ -93,6 +99,8 @@ public class LoanAssembler { this.loanCollateralAssembler = loanCollateralAssembler; this.loanSummaryWrapper = loanSummaryWrapper; this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; + this.holidayRepository = holidayRepository; + this.configurationDomainService = configurationDomainService; } public Loan assembleFrom(final JsonCommand command, final AppUser currentUser) { @@ -174,7 +182,9 @@ public class LoanAssembler { this.loanRepaymentScheduleTransactionProcessorFactory); final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); - final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, element); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List 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(), submittedOnDate, externalId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index 153ba90ce..e36397614 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -52,4 +52,6 @@ public interface LoanWritePlatformService { CommandProcessingResult removeLoanOfficer(Long loanId, JsonCommand command); void updateRepaymentsSchedule(Calendar calendar, Collection loanCalendarInstances); + + void applyHolidaysToLoans(); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index f75dbdd19..3dc5ed4da 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -12,18 +12,23 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; 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.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; 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.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; @@ -94,6 +99,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanScheduleGeneratorFactory loanScheduleFactory; private final CalendarInstanceRepository calendarInstanceRepository; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final HolidayRepository holidayRepository; + private final ConfigurationDomainService configurationDomainService; @Autowired public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -105,7 +112,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, - final PaymentDetailWritePlatformService paymentDetailWritePlatformService) { + final PaymentDetailWritePlatformService paymentDetailWritePlatformService, + final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanAssembler = loanAssembler; @@ -122,6 +130,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanScheduleFactory = loanScheduleFactory; this.calendarInstanceRepository = calendarInstanceRepository; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; + this.holidayRepository = holidayRepository; + this.configurationDomainService = configurationDomainService; } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { @@ -160,11 +170,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf //Recalculate first repayment date based in actual disbursement date. final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), actualDisbursementDate.toDate()); updateLoanCounters(loan, actualDisbursementDate); loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate); + existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate, isHolidayEnabled, holidays); if (!changes.isEmpty()) { 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 //assuming repayment schedule won't regenerate because expected disbursement and actual disbursement happens on same date final LocalDate firstRepaymentOnDate = null; + final boolean isHolidayEnabled = false; + final List holidays = null; loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate); + existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate, isHolidayEnabled, holidays); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -990,9 +1004,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } + @Transactional @Override public void updateRepaymentsSchedule(final Calendar calendar, Collection loanCalendarInstances) { + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final Collection loanStatuses = new ArrayList(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue())); final Collection loanTypes = new ArrayList(Arrays.asList(LoanType.GROUP.getValue(), LoanType.JLG.getValue())); @@ -1003,10 +1019,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } final List loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes); + List holidays = null; //loop through each loan to reschedule the repayment dates for (Loan loan : loans) { 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); } } @@ -1157,4 +1175,40 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } this.loanRepository.save(loansToUpdate); } + + @Transactional + @Override + public void applyHolidaysToLoans() { + + final Collection loanStatuses = new ArrayList(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 holidays = this.holidayRepository.findUnprocessed(); + + // Loop through all holidays + for (Holiday holiday : holidays) { + // All offices to which holiday is applied + final Set offices = holiday.getOffices(); + Collection officeIds = new ArrayList(offices.size()); + for (Office office : offices) { + officeIds.add(office.getId()); + } + + // get all loans + List loans = new ArrayList(); + //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); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyHolidaysToLoansScheduledJob.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyHolidaysToLoansScheduledJob.java new file mode 100644 index 000000000..b6673c792 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyHolidaysToLoansScheduledJob.java @@ -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()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java index 7df1030bd..219f827af 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java @@ -14,4 +14,6 @@ public interface ScheduledJobRunnerService { void updateLoanPaidInAdvance(); void applyAnnualFeeForSavings(); + + void applyHolidaysToLoans(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index f3367d66f..801d7d899 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -11,6 +11,7 @@ import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; import org.mifosplatform.infrastructure.core.service.DataSourcePerTenantService; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.security.service.TenantDetailsService; +import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService; import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,13 +28,16 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService private final TenantDetailsService tenantDetailsService; private final DataSourcePerTenantService dataSourcePerTenantService; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; + private final LoanWritePlatformService loanWritePlatformService; @Autowired public ScheduledJobRunnerServiceImpl(final DataSourcePerTenantService dataSourcePerTenantService, - final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { + final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService, + final LoanWritePlatformService loanWritePlatformService) { this.dataSourcePerTenantService = dataSourcePerTenantService; this.tenantDetailsService = tenantDetailsService; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; + this.loanWritePlatformService = loanWritePlatformService; } @Transactional @@ -220,4 +224,14 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info(tenant.getName() + ": Results affected by update: " + savingsIds.size()); } } + + @Transactional + @Override + public void applyHolidaysToLoans() { + List allTenants = this.tenantDetailsService.findAllTenants(); + for (MifosPlatformTenant tenant : allTenants) { + ThreadLocalContextUtil.setTenant(tenant); + this.loanWritePlatformService.applyHolidaysToLoans(); + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml index 744fb9447..28ec8b3ad 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml @@ -26,7 +26,11 @@ class="org.mifosplatform.scheduledjobs.ApplyAnnualFeeForSavingsScheduledJob"> - + + + + @@ -58,6 +62,14 @@ + + + + + + + @@ -83,6 +95,12 @@ + + + + + @@ -91,6 +109,7 @@ +