MIFOSX-449 - Sync repayment dates with meeting dates for group and JLG loans

This commit is contained in:
ashok-conflux
2013-06-20 12:56:39 +05:30
parent 348b469652
commit c2a5f8de61
16 changed files with 457 additions and 166 deletions
@@ -0,0 +1,4 @@
ALTER TABLE `m_loan`
ADD COLUMN `sync_disbursement_with_meeting` TINYINT(1) NULL AFTER `loan_transaction_strategy_id`;
@@ -9,6 +9,8 @@ import java.util.Collection;
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 CalendarInstanceRepository extends JpaRepository<CalendarInstance, Long>, JpaSpecificationExecutor<CalendarInstance> { public interface CalendarInstanceRepository extends JpaRepository<CalendarInstance, Long>, JpaSpecificationExecutor<CalendarInstance> {
@@ -16,4 +18,6 @@ public interface CalendarInstanceRepository extends JpaRepository<CalendarInstan
Collection<CalendarInstance> findByEntityIdAndEntityTypeId(Long entityId, Integer entityTypeId); Collection<CalendarInstance> findByEntityIdAndEntityTypeId(Long entityId, Integer entityTypeId);
@Query("from CalendarInstance ci where ci.entityId = :loanId and ci.entityTypeId = :entityTypeId")
CalendarInstance findCalendarInstaneByLoanId(@Param("loanId") Long loanId, @Param("entityTypeId") Integer entityTypeId);
} }
@@ -0,0 +1,18 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.calendar.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when Calendar resources are not found.
*/
public class MeetingFrequencyMismatchException extends AbstractPlatformDomainRuleException {
public MeetingFrequencyMismatchException(String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) {
super("error.msg.calendar." + postFix + ".not.the.same.as.meeting.frequency", defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -27,6 +27,8 @@ import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
public class CalendarHelper { public class CalendarHelper {
@@ -35,8 +37,21 @@ public class CalendarHelper {
final Recur recur = CalendarHelper.getICalRecur(recurringRule); final Recur recur = CalendarHelper.getICalRecur(recurringRule);
if (recur == null) { return null; } if (recur == null) { return null; }
final Date nextRecDate = recur.getNextDate(new Date(seedDate.toDate()), new Date(startDate.toDate())); final DateTime periodStart = new DateTime(startDate.toDate());
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded
final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate());
Date seed = null;
try {
seed = new Date(seedDateStr, "yyyy-MM-dd");
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
final Date nextRecDate = recur.getNextDate(seed, periodStart);
return nextRecDate == null ? null : new LocalDate(nextRecDate); return nextRecDate == null ? null : new LocalDate(nextRecDate);
} }
@@ -46,10 +61,16 @@ public class CalendarHelper {
final LocalDate periodEndDate = (endDate == null) ? DateUtils.getLocalDateOfTenant().plusYears(5) : endDate; final LocalDate periodEndDate = (endDate == null) ? DateUtils.getLocalDateOfTenant().plusYears(5) : endDate;
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate); return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate);
} }
public static Collection<LocalDate> getRecurringDatesFrom(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) {
final LocalDate periodStartDate = (startDate == null) ? DateUtils.getLocalDateOfTenant() : startDate;
final LocalDate periodEndDate = DateUtils.getLocalDateOfTenant().plusYears(5);
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate);
}
public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate, public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate,
final LocalDate periodStartDate, final LocalDate periodEndDate) { final LocalDate periodStartDate, final LocalDate periodEndDate) {
final int maxCount = 15;// Default number of recurring dates final int maxCount = 10;// Default number of recurring dates
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate, maxCount); return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate, maxCount);
} }
@@ -209,4 +230,79 @@ public class CalendarHelper {
return DayNameEnum.MO;//Default it to Monday return DayNameEnum.MO;//Default it to Monday
} }
} }
public static PeriodFrequencyType getMeetingPeriodFrequencyType(final String recurringRule){
final Recur recur = CalendarHelper.getICalRecur(recurringRule);
PeriodFrequencyType meetingFrequencyType = PeriodFrequencyType.INVALID;
if(recur.getFrequency().equals(Recur.DAILY)){
meetingFrequencyType = PeriodFrequencyType.DAYS;
}else if(recur.getFrequency().equals(Recur.WEEKLY)){
meetingFrequencyType = PeriodFrequencyType.WEEKS;
}else if(recur.getFrequency().equals(Recur.MONTHLY)){
meetingFrequencyType = PeriodFrequencyType.MONTHS;
}else if(recur.getFrequency().equals(Recur.YEARLY)){
meetingFrequencyType = PeriodFrequencyType.YEARS;
}
return meetingFrequencyType;
}
public static String getMeetingFrequencyFromPeriodFrequencyType(final PeriodFrequencyType periodFrequency){
String frequency = null;
if(periodFrequency.equals(PeriodFrequencyType.DAYS)){
frequency = Recur.DAILY;
}else if(periodFrequency.equals(PeriodFrequencyType.WEEKS)){
frequency = Recur.WEEKLY;
}else if(periodFrequency.equals(PeriodFrequencyType.MONTHS)){
frequency = Recur.MONTHLY;
}else if(periodFrequency.equals(PeriodFrequencyType.YEARS)){
frequency = Recur.YEARLY;
}
return frequency;
}
public static int getInterval(final String recurringRule){
final Recur recur = CalendarHelper.getICalRecur(recurringRule);
return recur.getInterval();
}
public static LocalDate getFirstRepaymentMeetingDate(final Calendar calendar, final LocalDate disbursementDate, final Integer loanRepaymentInterval, final String frequency){
final Recur recur = CalendarHelper.getICalRecur(calendar.getRecurrence());
if (recur == null) { return null; }
LocalDate startDate = disbursementDate;
final LocalDate seedDate = calendar.getStartDateLocalDate();
if(isValidRedurringDate(calendar.getRecurrence(), seedDate, startDate)){
startDate = startDate.plusDays(1);
}
//Recurring dates should follow loanRepaymentInterval.
//e.g.
// for weekly meeting interval is 1
//where as for loan product with fortnightly frequency interval is 2
//to generate currect set of meeting dates reset interval same as loan repayment interval.
recur.setInterval(loanRepaymentInterval);
//Recurring dates should follow loanRepayment frequency.
//e.g.
// daily meeting frequency should support all loan products with any frequency type.
//to generate currect set of meeting dates reset frequency same as loan repayment frequency.
if(recur.getFrequency().equals(Recur.DAILY)){
recur.setFrequency(frequency);
}
final DateTime periodStart = new DateTime(startDate.toDate());
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded
final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate());
Date seed = null;
try {
seed = new Date(seedDateStr, "yyyy-MM-dd");
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
final Date nextRecDate = recur.getNextDate(seed, periodStart);
final LocalDate firstRepaymentDate = nextRecDate == null ? null : new LocalDate(nextRecDate);
return firstRepaymentDate;
}
} }
@@ -96,7 +96,7 @@ public class LoansApiResource {
"summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors", "meeting", "productOptions", "summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors", "meeting", "productOptions",
"amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", "repaymentFrequencyTypeOptions", "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", "repaymentFrequencyTypeOptions",
"termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions",
"loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions")); "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions", "syncDisbursementWithMeeting"));
private final String resourceNameForPermissions = "LOAN"; private final String resourceNameForPermissions = "LOAN";
@@ -87,6 +87,7 @@ public class LoanAccountData {
private final Integer graceOnInterestCharged; private final Integer graceOnInterestCharged;
private final LocalDate interestChargedFromDate; private final LocalDate interestChargedFromDate;
private final LocalDate expectedFirstRepaymentOnDate; private final LocalDate expectedFirstRepaymentOnDate;
private final Boolean syncDisbursementWithMeeting;
// timeline // timeline
private final LoanApplicationTimelineData timeline; private final LoanApplicationTimelineData timeline;
@@ -190,6 +191,7 @@ public class LoanAccountData {
final Collection<StaffData> loanOfficerOptions = null; final Collection<StaffData> loanOfficerOptions = null;
final Collection<CodeValueData> loanPurposeOptions = null; final Collection<CodeValueData> loanPurposeOptions = null;
final Collection<CalendarData> calendarOptions = null; final Collection<CalendarData> calendarOptions = null;
final Boolean syncDisbursementWithMeeting = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -200,7 +202,8 @@ public class LoanAccountData {
feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData,
productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions,
syncDisbursementWithMeeting);
} }
/** /**
@@ -271,6 +274,7 @@ public class LoanAccountData {
final Collection<CodeValueData> loanPurposeOptions = null; final Collection<CodeValueData> loanPurposeOptions = null;
final Collection<CodeValueData> loanCollateralOptions = null; final Collection<CodeValueData> loanCollateralOptions = null;
final Collection<CalendarData> calendarOptions = null; final Collection<CalendarData> calendarOptions = null;
final Boolean syncDisbursementWithMeeting = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -281,7 +285,7 @@ public class LoanAccountData {
feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData,
productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting);
} }
public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) {
@@ -298,7 +302,7 @@ public class LoanAccountData {
acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions,
acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions,
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting);
} }
/** /**
@@ -370,7 +374,8 @@ public class LoanAccountData {
final Collection<CodeValueData> loanPurposeOptions = null; final Collection<CodeValueData> loanPurposeOptions = null;
final Collection<CodeValueData> loanCollateralOptions = null; final Collection<CodeValueData> loanCollateralOptions = null;
final Collection<CalendarData> calendarOptions = null; final Collection<CalendarData> calendarOptions = null;
final Boolean syncDisbursementWithMeeting = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments,
@@ -380,7 +385,8 @@ public class LoanAccountData {
feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData,
productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions,
syncDisbursementWithMeeting);
} }
public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) {
@@ -397,7 +403,8 @@ public class LoanAccountData {
acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions,
acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions,
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions,
acc.syncDisbursementWithMeeting);
} }
public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product,
@@ -444,6 +451,7 @@ public class LoanAccountData {
final LoanSummaryData summary = null; final LoanSummaryData summary = null;
final BigDecimal feeChargesDueAtDisbursementCharged = null; final BigDecimal feeChargesDueAtDisbursementCharged = null;
final ChargeData chargeTemplate = null; final ChargeData chargeTemplate = null;
final Boolean syncDisbursementWithMeeting = null;
final LoanScheduleData repaymentSchedule = null; final LoanScheduleData repaymentSchedule = null;
final Collection<LoanTransactionData> transactions = null; final Collection<LoanTransactionData> transactions = null;
@@ -474,7 +482,7 @@ public class LoanAccountData {
repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions,
repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions,
interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions,
loanPurposeOptions, loanCollateralOptions, calendarOptions); loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting);
} }
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
@@ -520,7 +528,7 @@ public class LoanAccountData {
acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors,
calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, acc.syncDisbursementWithMeeting);
} }
/* /*
@@ -539,7 +547,7 @@ public class LoanAccountData {
final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate,
final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary,
final BigDecimal feeChargesDueAtDisbursementCharged) { final BigDecimal feeChargesDueAtDisbursementCharged, final Boolean syncDisbursementWithMeeting) {
final LoanScheduleData repaymentSchedule = null; final LoanScheduleData repaymentSchedule = null;
final Collection<LoanTransactionData> transactions = null; final Collection<LoanTransactionData> transactions = null;
@@ -572,7 +580,8 @@ public class LoanAccountData {
feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData,
productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions,
syncDisbursementWithMeeting);
} }
/* /*
@@ -601,7 +610,8 @@ public class LoanAccountData {
acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData,
productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions,
acc.syncDisbursementWithMeeting);
} }
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions, public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
@@ -626,7 +636,8 @@ public class LoanAccountData {
acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions,
acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions,
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions,
acc.syncDisbursementWithMeeting);
} }
private LoanAccountData( private LoanAccountData(
@@ -672,7 +683,8 @@ public class LoanAccountData {
final Collection<EnumOptionData> interestTypeOptions, final Collection<EnumOptionData> interestCalculationPeriodTypeOptions, final Collection<EnumOptionData> interestTypeOptions, final Collection<EnumOptionData> interestCalculationPeriodTypeOptions,
final Collection<FundData> fundOptions, final Collection<ChargeData> chargeOptions, final ChargeData chargeTemplate, final Collection<FundData> fundOptions, final Collection<ChargeData> chargeOptions, final ChargeData chargeTemplate,
final Collection<StaffData> loanOfficerOptions, final Collection<CodeValueData> loanPurposeOptions, final Collection<StaffData> loanOfficerOptions, final Collection<CodeValueData> loanPurposeOptions,
final Collection<CodeValueData> loanCollateralOptions, final Collection<CalendarData> calendarOptions) { final Collection<CodeValueData> loanCollateralOptions, final Collection<CalendarData> calendarOptions,
final Boolean syncDisbursementWithMeeting) {
this.id = id; this.id = id;
this.accountNo = accountNo; this.accountNo = accountNo;
this.status = status; this.status = status;
@@ -714,6 +726,7 @@ public class LoanAccountData {
this.interestChargedFromDate = interestChargedFromDate; this.interestChargedFromDate = interestChargedFromDate;
this.timeline = timeline; this.timeline = timeline;
this.feeChargesAtDisbursementCharged = feeChargesDueAtDisbursementCharged; this.feeChargesAtDisbursementCharged = feeChargesDueAtDisbursementCharged;
this.syncDisbursementWithMeeting = syncDisbursementWithMeeting;
// totals // totals
this.summary = summary; this.summary = summary;
@@ -135,6 +135,9 @@ public class Loan extends AbstractPersistable<Long> {
@Column(name = "loan_status_id", nullable = false) @Column(name = "loan_status_id", nullable = false)
private Integer loanStatus; private Integer loanStatus;
@Column(name = "sync_disbursement_with_meeting", nullable = true)
private Boolean syncDisbursementWithMeeting;
// loan application states // loan application states
@Temporal(TemporalType.DATE) @Temporal(TemporalType.DATE)
@Column(name = "submittedon_date") @Column(name = "submittedon_date")
@@ -276,31 +279,34 @@ public class Loan extends AbstractPersistable<Long> {
final Set<LoanCollateral> collateral) { final Set<LoanCollateral> collateral) {
final LoanStatus status = null; final LoanStatus status = null;
final Group group = null; final Group group = null;
final Boolean syncDisbursementWithMeeting = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral); loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting);
} }
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final LoanProduct loanProduct, final Fund fund, final Staff officer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges) { final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Boolean syncDisbursementWithMeeting) {
final LoanStatus status = null; final LoanStatus status = null;
final CodeValue loanPurpose = null; final CodeValue loanPurpose = null;
final Set<LoanCollateral> collateral = null; final Set<LoanCollateral> collateral = null;
final Client client = null; final Client client = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral); loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting);
} }
public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group,
final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges) { final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Boolean syncDisbursementWithMeeting) {
final LoanStatus status = null; final LoanStatus status = null;
final CodeValue loanPurpose = null; final CodeValue loanPurpose = null;
final Set<LoanCollateral> collateral = null; final Set<LoanCollateral> collateral = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral); loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting);
} }
protected Loan() { protected Loan() {
@@ -310,7 +316,7 @@ public class Loan extends AbstractPersistable<Long> {
private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund,
final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus,
final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) { final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral, final Boolean syncDisbursementWithMeeting) {
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings();
@@ -348,6 +354,8 @@ public class Loan extends AbstractPersistable<Long> {
this.collateral = null; this.collateral = null;
} }
this.loanOfficerHistory = null; this.loanOfficerHistory = null;
this.syncDisbursementWithMeeting = syncDisbursementWithMeeting;
} }
private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) { private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) {
@@ -815,8 +823,7 @@ public class Loan extends AbstractPersistable<Long> {
} }
// add clientId, groupId and loanType changes to actual changes // add clientId, groupId and loanType changes to actual changes
// FIXME: AA - We may require separate api command to move loan from one
// client to another
final String clientIdParamName = "clientId"; final String clientIdParamName = "clientId";
final Long clientId = this.client == null ? null : this.client.getId(); final Long clientId = this.client == null ? null : this.client.getId();
if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) {
@@ -916,6 +923,13 @@ public class Loan extends AbstractPersistable<Long> {
} }
} }
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (command.isChangeInBooleanParameterNamed(syncDisbursementParameterName, isSyncDisbursementWithMeeting())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(syncDisbursementParameterName);
actualChanges.put(syncDisbursementParameterName, valueAsInput);
this.syncDisbursementWithMeeting = valueAsInput;
}
final String interestChargedFromDateParamName = "interestChargedFromDate"; final String interestChargedFromDateParamName = "interestChargedFromDate";
if (command.isChangeInLocalDateParameterNamed(interestChargedFromDateParamName, getInterestChargedFromDate())) { if (command.isChangeInLocalDateParameterNamed(interestChargedFromDateParamName, getInterestChargedFromDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(interestChargedFromDateParamName); final String valueAsInput = command.stringValueOfParameterNamed(interestChargedFromDateParamName);
@@ -1278,7 +1292,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 PaymentDetail paymentDetail) { final List<Long> existingReversedTransactionIds, final Map<String, Object> actualChanges,
final PaymentDetail paymentDetail, final LocalDate firstRepaymentMeetingDate) {
updateLoanToPreDisbursalState(); updateLoanToPreDisbursalState();
@@ -1304,7 +1319,7 @@ public class Loan extends AbstractPersistable<Long> {
handleDisbursementTransaction(paymentDetail, actualDisbursementDate); handleDisbursementTransaction(paymentDetail, actualDisbursementDate);
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) {
regenerateRepaymentSchedule(loanScheduleFactory, currency); regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate);
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
} else { } else {
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
@@ -1331,7 +1346,7 @@ 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 ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
@@ -1341,9 +1356,11 @@ public class Loan extends AbstractPersistable<Long> {
final Integer loanTermFrequency = this.termFrequency; final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType); final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
//If ExpectedFirstRepaymentOnDate is null and repayment is synced with meeting then use firstRepaymentMeetingDate
//to generate repayment schedule dates
final LocalDate firstRepaymentOnDate = (getExpectedFirstRepaymentOnDate() == null) ? firstRepaymentMeetingDate : getExpectedFirstRepaymentOnDate();
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency,
loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), getExpectedFirstRepaymentOnDate(), getInArrearsTolerance(), loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), firstRepaymentOnDate, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail); this.loanRepaymentScheduleDetail);
LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges); LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges);
@@ -2001,7 +2018,7 @@ public class Loan extends AbstractPersistable<Long> {
return disbursementDate; return disbursementDate;
} }
private LocalDate getExpectedFirstRepaymentOnDate() { public LocalDate getExpectedFirstRepaymentOnDate() {
LocalDate firstRepaymentDate = null; LocalDate firstRepaymentDate = null;
if (this.expectedFirstRepaymentOnDate != null) { if (this.expectedFirstRepaymentOnDate != null) {
firstRepaymentDate = new LocalDate(this.expectedFirstRepaymentOnDate); firstRepaymentDate = new LocalDate(this.expectedFirstRepaymentOnDate);
@@ -2299,4 +2316,9 @@ public class Loan extends AbstractPersistable<Long> {
this.loanSummaryWrapper = loanSummaryWrapper; this.loanSummaryWrapper = loanSummaryWrapper;
this.transactionProcessorFactory = transactionProcessorFactory; this.transactionProcessorFactory = transactionProcessorFactory;
} }
public boolean isSyncDisbursementWithMeeting() {
return this.syncDisbursementWithMeeting == null ? false : this.syncDisbursementWithMeeting;
}
} }
@@ -16,7 +16,13 @@ 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.portfolio.calendar.domain.Calendar;
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.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
@@ -45,18 +51,20 @@ public class LoanScheduleAssembler {
private final LoanChargeAssembler loanChargeAssembler; private final LoanChargeAssembler loanChargeAssembler;
private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final AprCalculator aprCalculator; private final AprCalculator aprCalculator;
private final CalendarRepository calendarRepository;
@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 LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.loanProductRepository = loanProductRepository; this.loanProductRepository = loanProductRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository; this.applicationCurrencyRepository = applicationCurrencyRepository;
this.loanScheduleFactory = loanScheduleFactory; this.loanScheduleFactory = loanScheduleFactory;
this.aprCalculator = aprCalculator; this.aprCalculator = aprCalculator;
this.loanChargeAssembler = loanChargeAssembler; this.loanChargeAssembler = loanChargeAssembler;
this.calendarRepository = calendarRepository;
} }
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
@@ -104,10 +112,41 @@ public class LoanScheduleAssembler {
// disbursement details // disbursement details
final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
final Money principalMoney = Money.of(currency, principal); final Money principalMoney = Money.of(currency, principal);
final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
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 (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);
}
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);
}
}
// grace details // grace details
final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element);
final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element);
@@ -125,7 +164,47 @@ public class LoanScheduleAssembler {
graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney);
} }
public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { 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 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) {
LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element);
return loanApplicationTerms.toLoanProductRelatedDetail(); return loanApplicationTerms.toLoanProductRelatedDetail();
} }
@@ -28,7 +28,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
public LoanScheduleModel calculateLoanSchedule(final JsonQuery query) { public LoanScheduleModel calculateLoanSchedule(final JsonQuery query) {
this.fromApiJsonDeserializer.validate(query.json()); this.fromApiJsonDeserializer.validate(query.json());
return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson()); return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson());
} }
} }
@@ -42,7 +42,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
"loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", "loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType",
"interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment",
"graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale",
"dateFormat", "charges", "collateral")); "dateFormat", "charges", "collateral", "syncDisbursementWithMeeting"));
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@@ -179,7 +179,26 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
} }
} }
} }
boolean meetingIdRequired = false;
//validate syncDisbursement
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element);
if(syncDisbursement == null){
baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false);
}else if(syncDisbursement.booleanValue()) {
meetingIdRequired = true;
}
}
final String calendarIdParameterName = "calendarId";
//if disbursement is synced then must have a meeting (calendar)
if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero();
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); } "Validation errors exist.", dataValidationErrors); }
} }
@@ -47,7 +47,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
"accountNo", "externalId", "fundId", "loanOfficerId", // optional "accountNo", "externalId", "fundId", "loanOfficerId", // optional
"loanPurposeId", "inArrearsTolerance", "charges", "collateral", // optional "loanPurposeId", "inArrearsTolerance", "charges", "collateral", // optional
"transactionProcessingStrategyId", // settings "transactionProcessingStrategyId", // settings
"calendarId" // optional "calendarId", // optional
"syncDisbursementWithMeeting"//optional
)); ));
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@@ -230,12 +231,6 @@ public final class LoanApplicationCommandFromApiJsonHelper {
baseDataValidator.reset().parameter(transactionProcessingStrategyIdParameterName).value(transactionProcessingStrategyId).notNull() baseDataValidator.reset().parameter(transactionProcessingStrategyIdParameterName).value(transactionProcessingStrategyId).notNull()
.integerGreaterThanZero(); .integerGreaterThanZero();
final String calendarIdParameterName = "calendarId";
if (fromApiJsonHelper.parameterExists(calendarIdParameterName, element)) {
final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).ignoreIfNull().integerGreaterThanZero();
}
// charges // charges
final String chargesParameterName = "charges"; final String chargesParameterName = "charges";
if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) { if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) {
@@ -302,6 +297,25 @@ public final class LoanApplicationCommandFromApiJsonHelper {
} }
} }
boolean meetingIdRequired = false;
//validate syncDisbursement
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element);
if(syncDisbursement == null){
baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false);
}else if(syncDisbursement.booleanValue()) {
meetingIdRequired = true;
}
}
final String calendarIdParameterName = "calendarId";
//if disbursement is synced then must have a meeting (calendar)
if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero();
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
} }
@@ -517,12 +531,6 @@ public final class LoanApplicationCommandFromApiJsonHelper {
.notExceedingLengthOf(500); .notExceedingLengthOf(500);
} }
final String calendarIdParameterName = "calendarId";
if (fromApiJsonHelper.parameterExists(calendarIdParameterName, element)) {
final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).ignoreIfNull().integerGreaterThanZero();
}
// charges // charges
final String chargesParameterName = "charges"; final String chargesParameterName = "charges";
if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) { if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) {
@@ -591,6 +599,25 @@ public final class LoanApplicationCommandFromApiJsonHelper {
} }
} }
boolean meetingIdRequired = false;
//validate syncDisbursement
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element);
if(syncDisbursement == null){
baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false);
}else if(syncDisbursement.booleanValue()) {
meetingIdRequired = true;
}
}
final String calendarIdParameterName = "calendarId";
//if disbursement is synced then must have a meeting (calendar)
if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero();
}
if (!atLeastOneParameterPassedForUpdate) { if (!atLeastOneParameterPassedForUpdate) {
final Object forceError = null; final Object forceError = null;
baseDataValidator.reset().anyOfNotNull(forceError); baseDataValidator.reset().anyOfNotNull(forceError);
@@ -639,4 +666,5 @@ public final class LoanApplicationCommandFromApiJsonHelper {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); } "Validation errors exist.", dataValidationErrors); }
} }
} }
@@ -21,6 +21,10 @@ import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -66,6 +70,23 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance) {
if (null != calendarInstance) {
Calendar calendar = calendarInstance.getCalendar();
if (calendar != null && actualDisbursementDate != null) {
// Disbursement date should fall on a meeting date
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), actualDisbursementDate)) {
final String errorMessage = "Expected disbursement date '" + actualDisbursementDate.toString()
+ "' does not fall on a meeting date.";
throw new NotValidRecurringDateException("loan.actual.disbursement.date", errorMessage, actualDisbursementDate.toString(),
calendar.getTitle());
}
}
}
}
public void validateTransaction(final String json) { public void validateTransaction(final String json) {
@@ -121,6 +142,22 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateRepaymentDateWithMeetingDate(LocalDate repaymentDate, final CalendarInstance calendarInstance) {
if (null != calendarInstance) {
Calendar calendar = calendarInstance.getCalendar();
if (calendar != null && repaymentDate != null) {
// Disbursement date should fall on a meeting date
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repaymentDate)) {
final String errorMessage = "Transaction date '" + repaymentDate.toString()
+ "' does not fall on a meeting date.";
throw new NotValidRecurringDateException("loan.transaction.date", errorMessage, repaymentDate.toString(),
calendar.getTitle());
}
}
}
}
private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
// Validate all string payment detail fields for max length // Validate all string payment detail fields for max length
final Integer paymentTypeId = fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); final Integer paymentTypeId = fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
@@ -12,7 +12,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.codes.domain.CodeValue; 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.api.JsonQuery; import org.mifosplatform.infrastructure.core.api.JsonQuery;
@@ -31,8 +30,6 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException;
import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
import org.mifosplatform.portfolio.client.domain.AccountNumberGenerator; import org.mifosplatform.portfolio.client.domain.AccountNumberGenerator;
import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory; import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory;
import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.Client;
@@ -48,7 +45,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTrans
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified;
import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -156,16 +152,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
// validate dates against calendar recurring dates
// get calendar
final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
validateCalendarDates(command, calendar);
}
final Loan newLoanApplication = loanAssembler.assembleFrom(command, currentUser); final Loan newLoanApplication = loanAssembler.assembleFrom(command, currentUser);
this.loanRepository.save(newLoanApplication); this.loanRepository.save(newLoanApplication);
@@ -184,12 +170,19 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
} }
// Save calendar instance // Save calendar instance
if (calendar != null) { final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(), CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(),
CalendarEntityType.LOANS.getValue()); CalendarEntityType.LOANS.getValue());
this.calendarInstanceRepository.save(calendarInstance); this.calendarInstanceRepository.save(calendarInstance);
} }
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(newLoanApplication.getId()) // .withEntityId(newLoanApplication.getId()) //
@@ -209,14 +202,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
this.fromApiJsonDeserializer.validateForModify(command.json()); this.fromApiJsonDeserializer.validateForModify(command.json());
final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
validateCalendarDates(command, calendar);
}
final Loan existingLoanApplication = retrieveLoanBy(loanId); final Loan existingLoanApplication = retrieveLoanBy(loanId);
if (!existingLoanApplication.isSubmittedAndPendingApproval()) { throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified( if (!existingLoanApplication.isSubmittedAndPendingApproval()) { throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified(
@@ -309,6 +294,13 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
this.noteRepository.save(note); this.noteRepository.save(note);
} }
final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
}
List<CalendarInstance> ciList = (List<CalendarInstance>) this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loanId, List<CalendarInstance> ciList = (List<CalendarInstance>) this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loanId,
CalendarEntityType.LOANS.getValue()); CalendarEntityType.LOANS.getValue());
if (calendar != null) { if (calendar != null) {
@@ -534,49 +526,4 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
return loan; return loan;
} }
private void validateCalendarDates(final JsonCommand command, final Calendar calendar) {
final LocalDate edDate = command.localDateValueOfParameterNamed("expectedDisbursementDate");
if (edDate != null) {
// Expected disbursement date should not be before Meeting start
// date
if (edDate.isBefore(calendar.getStartDateLocalDate())) {
final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' cannot be before meeting start date '"
+ calendar.getStartDate().toString() + "' ";
throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate,
calendar.getStartDate());
}
// Disbursement date should fall on a meeting date
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), edDate)) {
final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' does not fall on a meeting date.";
throw new NotValidRecurringDateException("loan.expected.disbursement.date", errorMessage, edDate.toString(),
calendar.getTitle());
}
}
final LocalDate repStartingDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate");
if (repStartingDate != null) {
// First repayment date should not be before Meeting date
if (repStartingDate.isBefore(calendar.getStartDateLocalDate())) {
final String errorMessage = "First repayment date '" + repStartingDate.toString()
+ "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' ";
throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage,
repStartingDate, calendar.getStartDate());
}
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repStartingDate)) {
final String errorMessage = "The First repayment date '" + repStartingDate.toString()
+ "' does not fall on a meeting date.";
throw new NotValidRecurringDateException("loan.first.repayment.date", errorMessage, repStartingDate.toString(),
calendar.getTitle());
}
}
}
} }
@@ -112,6 +112,7 @@ public class LoanAssembler {
final Long loanOfficerId = fromApiJsonHelper.extractLongNamed("loanOfficerId", element); final Long loanOfficerId = fromApiJsonHelper.extractLongNamed("loanOfficerId", element);
final Long transactionProcessingStrategyId = fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element); final Long transactionProcessingStrategyId = fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element);
final Long loanPurposeId = fromApiJsonHelper.extractLongNamed("loanPurposeId", element); final Long loanPurposeId = fromApiJsonHelper.extractLongNamed("loanPurposeId", element);
final Boolean syncDisbursementWithMeeting = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element);
final LoanProduct loanProduct = this.loanProductRepository.findOne(productId); final LoanProduct loanProduct = this.loanProductRepository.findOne(productId);
if (loanProduct == null) { throw new LoanProductNotFoundException(productId); } if (loanProduct == null) { throw new LoanProductNotFoundException(productId); }
@@ -154,7 +155,7 @@ public class LoanAssembler {
if (group == null) { throw new GroupNotFoundException(groupId); } if (group == null) { throw new GroupNotFoundException(groupId); }
loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer,
loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting);
} }
if (client != null && group != null) { if (client != null && group != null) {
@@ -162,7 +163,7 @@ public class LoanAssembler {
if (!group.hasClientAsMember(client)) { throw new ClientNotInGroupException(clientId, groupId); } if (!group.hasClientAsMember(client)) { throw new ClientNotInGroupException(clientId, groupId); }
loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(),
loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting);
} }
final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element); final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element);
@@ -442,7 +442,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
+ " la.fee_charges_overdue_derived as feeChargesOverdue," + " la.fee_charges_overdue_derived as feeChargesOverdue,"
+ " la.penalty_charges_overdue_derived as penaltyChargesOverdue," + " la.penalty_charges_overdue_derived as penaltyChargesOverdue,"
+ " la.total_overdue_derived as totalOverdue," + " la.total_overdue_derived as totalOverdue,"
+ " la.overdue_since_date_derived as overdueSinceDate" + " la.overdue_since_date_derived as overdueSinceDate,"
+ " l.sync_disbursement_with_meeting as syncDisbursementWithMeeting"
+ " from m_loan l" // + " from m_loan l" //
+ " join m_product_loan lp on lp.id = l.product_id" // + " join m_product_loan lp on lp.id = l.product_id" //
+ " join m_currency rc on rc.`code` = l.currency_code" // + " join m_currency rc on rc.`code` = l.currency_code" //
@@ -584,6 +585,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LocalDate expectedFirstRepaymentOnDate = JdbcSupport.getLocalDate(rs, "expectedFirstRepaymentOnDate"); final LocalDate expectedFirstRepaymentOnDate = JdbcSupport.getLocalDate(rs, "expectedFirstRepaymentOnDate");
final LocalDate interestChargedFromDate = JdbcSupport.getLocalDate(rs, "interestChargedFromDate"); final LocalDate interestChargedFromDate = JdbcSupport.getLocalDate(rs, "interestChargedFromDate");
final Boolean syncDisbursementWithMeeting = rs.getBoolean("syncDisbursementWithMeeting");
final BigDecimal feeChargesDueAtDisbursementCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, final BigDecimal feeChargesDueAtDisbursementCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
"feeChargesDueAtDisbursementCharged"); "feeChargesDueAtDisbursementCharged");
LoanSummaryData loanSummary = null; LoanSummaryData loanSummary = null;
@@ -652,7 +655,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId,
amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType,
interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment,
graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged,
syncDisbursementWithMeeting);
} }
} }
@@ -8,7 +8,6 @@ package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
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;
@@ -29,7 +28,6 @@ import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException;
import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.calendar.service.CalendarHelper;
import org.mifosplatform.portfolio.charge.domain.Charge; import org.mifosplatform.portfolio.charge.domain.Charge;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
@@ -57,7 +55,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
@@ -65,6 +62,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFound
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.mifosplatform.portfolio.note.domain.Note; import org.mifosplatform.portfolio.note.domain.Note;
import org.mifosplatform.portfolio.note.domain.NoteRepository; import org.mifosplatform.portfolio.note.domain.NoteRepository;
@@ -74,7 +72,6 @@ import org.mifosplatform.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Service @Service
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
@@ -139,12 +136,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanEventApiJsonValidator.validateDisbursement(command.json()); this.loanEventApiJsonValidator.validateDisbursement(command.json());
final Loan loan = retrieveLoanBy(loanId); final Loan loan = retrieveLoanBy(loanId);
// validate actual disbursement date against meeting date
// validate actual disbursement date against meeting date final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
Collection<CalendarInstance> calendarInstances = this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loan.getId(),
CalendarEntityType.LOANS.getValue()); CalendarEntityType.LOANS.getValue());
validateDisbursementDate(command.localDateValueOfParameterNamed("actualDisbursementDate"), calendarInstances); if(loan.isSyncDisbursementWithMeeting()){
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance);
}
final MonetaryCurrency currency = loan.getCurrency(); final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
@@ -155,8 +155,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
//Recalculate first repayment date based in actual disbursement date.
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance);
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -181,36 +185,39 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
private void validateDisbursementDate(final LocalDate disbursementDate, final Collection<CalendarInstance> calendarInstances) { private LocalDate getFirstRepaymentMeetingDate(
if (!CollectionUtils.isEmpty(calendarInstances)) { final LocalDate actualDisbursementDate, final Loan loan,
Calendar calendar = null; final CalendarInstance calendarInstance) {
for (CalendarInstance calendarInstance : calendarInstances) { final Calendar calendar = (calendarInstance == null) ? null
calendar = calendarInstance.getCalendar(); : calendarInstance.getCalendar();
break; LocalDate firstRepaymentMeetingDate = null;
} if (calendar != null) {// sync repayments
if (calendar != null && disbursementDate != null) {
// Actual disbursement date cannot be before Meeting start
// date
if (disbursementDate.isBefore(calendar.getStartDateLocalDate())) {
final String errorMessage = "Actual disbursement date '" + disbursementDate.toString()
+ "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' ";
throw new LoanApplicationDateException("actual.disbursement.date.cannot.be.before.meeting.start.date", errorMessage,
disbursementDate.toString(), calendar.getStartDate());
}
// Disbursement date should fall on a meeting date
if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), disbursementDate)) {
final String errorMessage = "Expected disbursement date '" + disbursementDate.toString()
+ "' does not fall on a meeting date.";
throw new NotValidRecurringDateException("loan.actual.disbursement.date", errorMessage, disbursementDate.toString(),
calendar.getTitle());
}
}
}
}
// TODO: AA - user provided first repayment date takes precedence over recalculated meeting date
if (loan.getExpectedFirstRepaymentOnDate() == 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 LoanProductRelatedDetail repaymentScheduleDetails = loan
.repaymentScheduleDetail();
if (repaymentScheduleDetails != null) {// Not expecting to be
// null
final Integer repayEvery = repaymentScheduleDetails
.getRepayEvery();
final String frequency = CalendarHelper
.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails
.getRepaymentPeriodFrequencyType());
firstRepaymentMeetingDate = CalendarHelper
.getFirstRepaymentMeetingDate(calendar,
actualDisbursementDate, repayEvery,
frequency);
}
}
}
return firstRepaymentMeetingDate;
}
/**** /****
* TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base
*****/ *****/
@@ -235,8 +242,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
// Bulk disbursement should happen on meeting date (mostly from collection sheet).
//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;
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, paymentDetail); existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
this.loanRepository.save(loan); this.loanRepository.save(loan);
@@ -327,7 +338,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private CommandProcessingResultBuilder saveLoanRepayment(final Long loanId, final PaymentDetail paymentDetail, private CommandProcessingResultBuilder saveLoanRepayment(final Long loanId, final PaymentDetail paymentDetail,
final BigDecimal transactionAmount, final LocalDate transactionDate, final String noteText) { final BigDecimal transactionAmount, final LocalDate transactionDate, final String noteText) {
final Loan loan = retrieveLoanBy(loanId); final Loan loan = retrieveLoanBy(loanId);
//TODO: Is it required to validate transaction date with meeting dates if repayments is synced with meeting?
/*if(loan.isSyncDisbursementWithMeeting()){
// validate actual disbursement date against meeting date
CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
CalendarEntityType.LOANS.getValue());
this.loanEventApiJsonValidator.validateRepaymentDateWithMeetingDate(transactionDate, calendarInstance);
}*/
final List<Long> existingTransactionIds = new ArrayList<Long>(); final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>(); final List<Long> existingReversedTransactionIds = new ArrayList<Long>();