MIFOSX-1584 Added business logic for minimum number of days between disbursal day and first repayment date

This commit is contained in:
Nayan
2014-10-24 16:20:41 +05:30
parent e61abec3de
commit 1c5364ac7f
17 changed files with 525 additions and 60 deletions
@@ -0,0 +1,201 @@
/**
* 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.integrationtests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mifosplatform.integrationtests.common.CalendarHelper;
import org.mifosplatform.integrationtests.common.ClientHelper;
import org.mifosplatform.integrationtests.common.CommonConstants;
import org.mifosplatform.integrationtests.common.GroupHelper;
import org.mifosplatform.integrationtests.common.Utils;
import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder;
import org.mifosplatform.integrationtests.common.loans.LoanStatusChecker;
import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper;
import org.mifosplatform.integrationtests.common.savings.SavingsAccountHelper;
import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.builder.ResponseSpecBuilder;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.specification.RequestSpecification;
import com.jayway.restassured.specification.ResponseSpecification;
/**
* Test the creation, approval and rejection of a loan reschedule request
**/
@SuppressWarnings({ "rawtypes" })
public class MinimumDaysBetweenDisbursalAndFirstRepaymentTest {
private ResponseSpecification responseSpec;
private ResponseSpecification responseSpecForStatusCode403;
private RequestSpecification requestSpec;
private LoanTransactionHelper loanTransactionHelper;
private Integer clientId;
private Integer groupId;
private Integer groupCalendarId;
private Integer loanProductId;
private Integer loanId;
private String loanPrincipalAmount = "100000.00";
private String numberOfRepayments = "12";
private String interestRatePerPeriod = "18";
private String groupActivationDate = "1 August 2014";
@Before
public void setup() {
Utils.initializeRESTAssured();
}
/*
* MinimumDaysBetweenDisbursalAndFirstRepayment is set to 7 days and days
* between disbursal date and first repayment is set as 7. system should
* allow to create this loan and allow to disburse
*/
@Test
public void createLoanEntity_WITH_DAY_BETWEEN_DISB_DATE_AND_REPAY_START_DATE_GREATER_THAN_MIN_DAY_CRITERIA() {
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
// create all required entities
this.createRequiredEntities();
final String disbursalDate = "4 September 2014";
final String firstRepaymentDate = "11 September 2014";
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(loanPrincipalAmount)
.withLoanTermFrequency(numberOfRepayments).withLoanTermFrequencyAsWeeks().withNumberOfRepayments(numberOfRepayments)
.withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments()
.withInterestCalculationPeriodTypeAsDays().withInterestRatePerPeriod(interestRatePerPeriod)
.withRepaymentFrequencyTypeAsWeeks().withSubmittedOnDate(disbursalDate).withExpectedDisbursementDate(disbursalDate)
.withPrincipalGrace("2").withInterestGrace("2").withFirstRepaymentDate(firstRepaymentDate)
.build(this.clientId.toString(), this.loanProductId.toString(), null);
this.loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
// Test for loan account is created
Assert.assertNotNull(this.loanId);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, this.loanId);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
// Test for loan account is created, can be approved
this.loanTransactionHelper.approveLoan(disbursalDate, this.loanId);
loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, this.loanId);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
// Test for loan account approved can be disbursed
this.loanTransactionHelper.disburseLoan(disbursalDate, this.loanId);
loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, this.loanId);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
}
/*
* MinimumDaysBetweenDisbursalAndFirstRepayment is set to 7 days and days
* between disbursal date and first repayment is set as 7. system should
* allow to create this loan and allow to disburse
*/
@SuppressWarnings("unchecked")
@Test
public void createLoanEntity_WITH_DAY_BETWEEN_DISB_DATE_AND_REPAY_START_DATE_LESS_THAN_MIN_DAY_CRITERIA() {
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.responseSpecForStatusCode403 = new ResponseSpecBuilder().expectStatusCode(403).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
// create all required entities
this.createRequiredEntities();
// loanTransactionHelper is reassigned to accept 403 status code from
// server
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpecForStatusCode403);
final String disbursalDate = "4 September 2014";
final String firstRepaymentDate = "5 September 2014";
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(loanPrincipalAmount)
.withLoanTermFrequency(numberOfRepayments).withLoanTermFrequencyAsWeeks().withNumberOfRepayments(numberOfRepayments)
.withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments()
.withInterestCalculationPeriodTypeAsDays().withInterestRatePerPeriod(interestRatePerPeriod)
.withRepaymentFrequencyTypeAsWeeks().withSubmittedOnDate(disbursalDate).withExpectedDisbursementDate(disbursalDate)
.withPrincipalGrace("2").withInterestGrace("2").withFirstRepaymentDate(firstRepaymentDate)
.build(this.clientId.toString(), this.loanProductId.toString(), null);
List<HashMap> error = (List<HashMap>) this.loanTransactionHelper.createLoanAccount(loanApplicationJSON,
CommonConstants.RESPONSE_ERROR);
assertEquals("validation.msg.loan.days.between.first.repayment.and.disbursal.are.less than.minimum.number.of.days.required", error
.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
}
/**
* Creates the client, loan product, and loan entities
**/
private void createRequiredEntities() {
final String minimumDaysBetweenDisbursalAndFirstRepayment = "7"; // &
// days
this.createGroupEntityWithCalendar();
this.createClientEntity();
this.associateClientToGroup(this.groupId, this.clientId);
this.createLoanProductEntity(minimumDaysBetweenDisbursalAndFirstRepayment);
}
/*
* Associate client to the group
*/
private void associateClientToGroup(final Integer groupId, final Integer clientId) {
GroupHelper.associateClient(this.requestSpec, this.responseSpec, groupId.toString(), clientId.toString());
GroupHelper.verifyGroupMembers(this.requestSpec, this.responseSpec, groupId, clientId);
}
/*
* Create a new group
*/
private void createGroupEntityWithCalendar() {
this.groupId = GroupHelper.createGroup(this.requestSpec, this.responseSpec, this.groupActivationDate);
GroupHelper.verifyGroupCreatedOnServer(this.requestSpec, this.responseSpec, this.groupId);
final String startDate = this.groupActivationDate;
final String frequency = "2"; // 2:Weekly
final String interval = "1"; // Every one week
final String repeatsOnDay = "1"; // 1:Monday
this.groupCalendarId = CalendarHelper.createMeetingCalendarForGroup(this.requestSpec, this.responseSpec, this.groupId, startDate,
frequency, interval, repeatsOnDay);
}
/**
* create a new client
**/
private void createClientEntity() {
this.clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, this.clientId);
}
/**
* create a new loan product
**/
private void createLoanProductEntity(final String minimumDaysBetweenDisbursalAndFirstRepayment) {
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(loanPrincipalAmount)
.withNumberOfRepayments(numberOfRepayments).withinterestRatePerPeriod(interestRatePerPeriod)
.withInterestRateFrequencyTypeAsYear()
.withMinimumDaysBetweenDisbursalAndFirstRepayment(minimumDaysBetweenDisbursalAndFirstRepayment).build(null);
this.loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
}
}
@@ -0,0 +1,61 @@
/**
* 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.integrationtests.common;
import static com.jayway.restassured.path.json.JsonPath.from;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import com.google.gson.Gson;
import com.jayway.restassured.specification.RequestSpecification;
import com.jayway.restassured.specification.ResponseSpecification;
public class CalendarHelper {
private static final String BASE_URL = "/mifosng-provider/api/v1/";
private static final String PARENT_ENTITY_NAME = "groups/";
private static final String ENITY_NAME = "/calendars";
public static Integer createMeetingCalendarForGroup(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer groupId, final String startDate, final String frequency, final String interval, final String repeatsOnDay) {
System.out.println("---------------------------------CREATING A MEETING CALENDAR FOR THE GROUP------------------------------");
final String CALENDAR_RESOURCE_URL = BASE_URL + PARENT_ENTITY_NAME + groupId + ENITY_NAME + "?" + Utils.TENANT_IDENTIFIER;
System.out.println(CALENDAR_RESOURCE_URL);
return Utils.performServerPost(requestSpec, responseSpec, CALENDAR_RESOURCE_URL, getTestCalendarAsJSON(frequency, interval, repeatsOnDay, startDate),
"resourceId");
}
public static String getTestCalendarAsJSON(final String frequency, final String interval,final String repeatsOnDay,
final String startDate) {
final HashMap<String, String> map = new HashMap<>();
map.put("dateFormat", "dd MMMM yyyy");
map.put("locale", "en");
map.put("frequency", frequency);
map.put("interval", interval);
map.put("repeating", "true");
map.put("repeatsOnDay", repeatsOnDay );
map.put("title", Utils.randomNameGenerator("groups_CollectionMeeting", 4));
map.put("typeId", "1");
map.put("startDate", startDate);
System.out.println("map : " + map);
return new Gson().toJson(map);
}
public static void verifyCalendarCreatedOnServer(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer generatedGroupId, final Integer generatedCalendarId) {
System.out.println("------------------------------CHECK CALENDAR DETAILS------------------------------------\n");
final String CLIENT_URL = "/mifosng-provider/api/v1/groups/" + generatedGroupId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
final String responseCalendarDetailsinJSON = Utils.performServerGet(requestSpec, responseSpec, CLIENT_URL, "collectionMeetingCalendar");
final Integer responseCalendarId = from(responseCalendarDetailsinJSON).get("id");
assertEquals("ERROR IN CREATING THE CALENDAR", generatedCalendarId, responseCalendarId);
}
}
@@ -47,6 +47,7 @@ public class LoanApplicationTestBuilder {
private List<HashMap> disbursementData = null; private List<HashMap> disbursementData = null;
private List<HashMap> charges = new ArrayList<HashMap>(); private List<HashMap> charges = new ArrayList<HashMap>();
private String recalculationRestFrequencyDate = null; private String recalculationRestFrequencyDate = null;
private String repaymentsStartingFromDate = null;
public String build(final String ID, final String loanProductId, final String savingsID) { public String build(final String ID, final String loanProductId, final String savingsID) {
@@ -73,6 +74,8 @@ public class LoanApplicationTestBuilder {
map.put("expectedDisbursementDate", this.expectedDisbursmentDate); map.put("expectedDisbursementDate", this.expectedDisbursmentDate);
map.put("submittedOnDate", this.submittedOnDate); map.put("submittedOnDate", this.submittedOnDate);
map.put("loanType", this.loanType); map.put("loanType", this.loanType);
map.put("repaymentsStartingFromDate", this.repaymentsStartingFromDate);
if (charges != null) { if (charges != null) {
map.put("charges", charges); map.put("charges", charges);
} }
@@ -240,4 +243,9 @@ public class LoanApplicationTestBuilder {
this.recalculationRestFrequencyDate = recalculationRestFrequencyDate; this.recalculationRestFrequencyDate = recalculationRestFrequencyDate;
return this; return this;
} }
public LoanApplicationTestBuilder withFirstRepaymentDate(final String firstRepaymentDate) {
this.repaymentsStartingFromDate = firstRepaymentDate;
return this;
}
} }
@@ -87,6 +87,7 @@ public class LoanProductTestBuilder {
private String recalculationRestFrequencyType = "1"; private String recalculationRestFrequencyType = "1";
private String recalculationRestFrequencyInterval = "0"; private String recalculationRestFrequencyInterval = "0";
private String recalculationRestFrequencyDate = null; private String recalculationRestFrequencyDate = null;
private String minimumDaysBetweenDisbursalAndFirstRepayment = "0";
public String build(final String chargeId) { public String build(final String chargeId) {
final HashMap<String, Object> map = new HashMap<>(); final HashMap<String, Object> map = new HashMap<>();
@@ -120,6 +121,7 @@ public class LoanProductTestBuilder {
map.put("minPrincipal", this.minPrincipal); map.put("minPrincipal", this.minPrincipal);
map.put("maxPrincipal", this.maxPrincipal); map.put("maxPrincipal", this.maxPrincipal);
map.put("overdueDaysForNPA", this.overdueDaysForNPA); map.put("overdueDaysForNPA", this.overdueDaysForNPA);
map.put("minimumDaysBetweenDisbursalAndFirstRepayment", this.minimumDaysBetweenDisbursalAndFirstRepayment);
if (multiDisburseLoan) { if (multiDisburseLoan) {
map.put("multiDisburseLoan", this.multiDisburseLoan); map.put("multiDisburseLoan", this.multiDisburseLoan);
map.put("maxTrancheCount", this.maxTrancheCount); map.put("maxTrancheCount", this.maxTrancheCount);
@@ -377,4 +379,8 @@ public class LoanProductTestBuilder {
return this; return this;
} }
public LoanProductTestBuilder withMinimumDaysBetweenDisbursalAndFirstRepayment(final String minimumDaysBetweenDisbursalAndFirstRepayment){
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
return this;
}
} }
@@ -49,6 +49,10 @@ public class LoanTransactionHelper {
return Utils.performServerPost(this.requestSpec, this.responseSpec, APPLY_LOAN_URL, loanApplicationJSON, "loanId"); return Utils.performServerPost(this.requestSpec, this.responseSpec, APPLY_LOAN_URL, loanApplicationJSON, "loanId");
} }
public Object createLoanAccount(final String loanApplicationJSON, final String responseAttribute) {
return Utils.performServerPost(this.requestSpec, this.responseSpec, APPLY_LOAN_URL, loanApplicationJSON, responseAttribute);
}
public Integer updateLoan(final Integer id, final String loanApplicationJSON) { public Integer updateLoan(final Integer id, final String loanApplicationJSON) {
return Utils.performServerPut(this.requestSpec, this.responseSpec, "/mifosng-provider/api/v1/loans/" + id return Utils.performServerPut(this.requestSpec, this.responseSpec, "/mifosng-provider/api/v1/loans/" + id
+ "?" + Utils.TENANT_IDENTIFIER, loanApplicationJSON, "loanId"); + "?" + Utils.TENANT_IDENTIFIER, loanApplicationJSON, "loanId");
@@ -130,6 +130,10 @@ public class DataValidatorBuilder {
} }
/*** FIXME: Vishwas, why does this method have a parameter? Seems wrong ***/ /*** FIXME: Vishwas, why does this method have a parameter? Seems wrong ***/
/*
* This method is not meant for validation, if you have mandatory boolean param and
* if it has invalid value or value not passed then call this method, this method is always used with input as false
*/
public DataValidatorBuilder trueOrFalseRequired(final boolean trueOfFalseFieldProvided) { public DataValidatorBuilder trueOrFalseRequired(final boolean trueOfFalseFieldProvided) {
if (!trueOfFalseFieldProvided && !this.ignoreNullValue) { if (!trueOfFalseFieldProvided && !this.ignoreNullValue) {
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(this.resource).append(".") final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(this.resource).append(".")
@@ -58,7 +58,15 @@ public enum PeriodFrequencyType {
} }
public boolean isYearly() { public boolean isYearly() {
return this.value.equals(PeriodFrequencyType.MONTHS.getValue()); return this.value.equals(PeriodFrequencyType.YEARS.getValue());
}
public boolean isWeekly() {
return this.value.equals(PeriodFrequencyType.WEEKS.getValue());
}
public boolean isDaily() {
return this.value.equals(PeriodFrequencyType.DAYS.getValue());
} }
public boolean isInvalid() { public boolean isInvalid() {
@@ -9,9 +9,9 @@ import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRul
public class LoanDisbursalException extends AbstractPlatformDomainRuleException { public class LoanDisbursalException extends AbstractPlatformDomainRuleException {
public LoanDisbursalException(final String cureentProduct, final String restrictedProduct) { public LoanDisbursalException(final String currentProduct, final String restrictedProduct) {
super("error.msg.loan.disbursal.failed", "This loan could not be disbursed as `" + cureentProduct + "` and `" + restrictedProduct super("error.msg.loan.disbursal.failed", "This loan could not be disbursed as `" + currentProduct + "` and `" + restrictedProduct
+ "` are not allowed to co-exist", new Object[] { cureentProduct, restrictedProduct }); + "` are not allowed to co-exist", new Object[] { currentProduct, restrictedProduct });
} }
public LoanDisbursalException(final String defaultUserMessage, final String entity, final Object... defaultUserMessageArgs) { public LoanDisbursalException(final String defaultUserMessage, final String entity, final Object... defaultUserMessageArgs) {
@@ -0,0 +1,22 @@
/**
* 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.loanaccount.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* {@link AbstractPlatformDomainRuleException} thrown when a number of days between disbursal date and firstRepayment is less than
* minimumDaysBetweenDisbursalAndFirstRepayment
*
*/
public class MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException extends AbstractPlatformDomainRuleException {
public MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException(final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
super("validation.msg.loan.days.between.first.repayment.and.disbursal.are.less than.minimum.number.of.days.required",
defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -28,6 +28,7 @@ import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays; import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil; import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.accountdetails.domain.AccountType;
import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
@@ -50,6 +51,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
@@ -160,44 +162,97 @@ public class LoanScheduleAssembler {
final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate; LocalDate calculatedRepaymentsStartingFromDate = null;
final Boolean synchDisbursement = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final Boolean synchDisbursement = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element);
final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element); final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element);
Calendar calendar = null; Calendar calendar = null;
if ((synchDisbursement != null && synchDisbursement.booleanValue()) || (calendarId != null && calendarId != 0)) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
// validate repayment frequency and interval with meeting frequency final String loanTypeParameterName = "loanType";
// and interval final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element);
final PeriodFrequencyType meetingPeriodFrequency = CalendarUtils.getMeetingPeriodFrequencyType(calendar.getRecurrence());
validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType, final AccountType loanType = AccountType.fromName(loanTypeStr);
CalendarUtils.getInterval(calendar.getRecurrence()), repaymentEvery);
/*
* If it is JLG loan then make sure loan frequency is same as
* Group/Center meeting frequency or multiple of it. TODO: Check should
* be either same frequency or loan freq is multiple of center/group
* meeting freq multiples
*/
if (loanType.isJLGAccount()) {
if ((synchDisbursement != null && synchDisbursement.booleanValue())) {
calendar = this.calendarRepository.findOne(calendarId);
if (calendar == null) { throw new CalendarNotFoundException(calendarId); }
final PeriodFrequencyType meetingPeriodFrequency = CalendarUtils.getMeetingPeriodFrequencyType(calendar.getRecurrence());
validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType,
CalendarUtils.getInterval(calendar.getRecurrence()), repaymentEvery);
}
} }
if (synchDisbursement != null && synchDisbursement.booleanValue()) { /*
validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); * If user has not passed the first repayments date then only system
* will derive it based on loan is individual/group loan or JLG loan.
*/
Boolean isRepaymentsStartingFromDateDerived = false;
if (repaymentsStartingFromDate == null) {
calculatedRepaymentsStartingFromDate = deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
repaymentPeriodFrequencyType, loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar);
isRepaymentsStartingFromDateDerived = true;
} }
// if calendar is attached (not null) then reschedule repayment dates /*
// according to meeting * If it is JLG loan, then make sure first repayment falls on meeting
if (null != calendar) { * date
// TODO : AA - Is it require to reset repaymentsStartingFromDate or */
// set only if it is not provided (or null) if (loanType.isJLGAccount()) {
// Currently provided repaymentsStartingFromDate takes precedence if (isRepaymentsStartingFromDateDerived) {
// over system generated next meeting date validateRepaymentsStartDateWithMeetingDates(calculatedRepaymentsStartingFromDate, calendar);
if (calculatedRepaymentsStartingFromDate == null) { } else {
// 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 = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate,
repaymentEvery, frequency);
} else {// validate user provided repaymentsStartFromDate
validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar); validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar);
} }
} }
/*
* If it is JLG loan and disbursement is synced on meeting, make sure
* disbursement date on meeting date
*/
if (loanType.isJLGAccount()) {
if (synchDisbursement != null && synchDisbursement.booleanValue()) {
validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar);
}
}
/*
* If minimum days between disbursal and first repayment is set, then
* validate the rule
*/
if (isRepaymentsStartingFromDateDerived) {
validateMinimumDaysBetweenDisbursalAndFirstRepayment(expectedDisbursementDate, calculatedRepaymentsStartingFromDate,
loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment());
} else {
validateMinimumDaysBetweenDisbursalAndFirstRepayment(expectedDisbursementDate, repaymentsStartingFromDate,
loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment());
}
/*
* If first repayment is not derived and instead directly taken from
* input then copy the value of repaymentsStartingFromDate to
* calculatedRepaymentsStartingFromDate
*
* TODO: The reason for this bad code is, LoanApplicationTerms.assembleFrom
* takes both repaymentsStartingFromDate and calculatedRepaymentsStartingFromDate
* and derived data has precedence over input date,
*
* FIXME:LoanApplicationTerms.assembleFrom should take only one
* "first repayment date" than taking two dates with same purpose.
*
*/
if (!isRepaymentsStartingFromDateDerived) {
calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate;
}
// 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);
@@ -424,4 +479,66 @@ public class LoanScheduleAssembler {
} }
} }
} }
private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Integer repaymentEvery,
final LocalDate expectedDisbursementDate, final PeriodFrequencyType repaymentPeriodFrequencyType,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar){
LocalDate derivedFirstRepayment = null;
LocalDate dateBasedOnRepaymentFrequency = null;
final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
if (loanType.isIndividualAccount() || loanType.isGroupAccount()) {
// Derive the first repayment date as greater date among (disbursement date + plus frequency) or
// (disbursement date + minimum between disbursal and first repayment )
if (repaymentPeriodFrequencyType.isDaily()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusDays(repaymentEvery);
}
else if(repaymentPeriodFrequencyType.isWeekly()){
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusWeeks(repaymentEvery);
}
else if (repaymentPeriodFrequencyType.isMonthly()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusMonths(repaymentEvery);
}
else if (repaymentPeriodFrequencyType.isYearly()){
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusYears(repaymentEvery);
}
derivedFirstRepayment = dateBasedOnRepaymentFrequency.isAfter(dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment)?dateBasedOnRepaymentFrequency:dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment;
}
else if(loanType.isJLGAccount()){
final LocalDate refernceDateForCalculatingFirstRepaymentDate = expectedDisbursementDate;
derivedFirstRepayment = deriveFirstRepaymentDateForJLGLoans(repaymentEvery, expectedDisbursementDate,refernceDateForCalculatingFirstRepaymentDate,
repaymentPeriodFrequencyType,minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
}
return derivedFirstRepayment;
}
private LocalDate deriveFirstRepaymentDateForJLGLoans(final Integer repaymentEvery,
final LocalDate expectedDisbursementDate, final LocalDate refernceDateForCalculatingFirstRepaymentDate,
final PeriodFrequencyType repaymentPeriodFrequencyType,final Integer minimumDaysBetweenDisbursalAndFirstRepayment,
final Calendar calendar){
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar, refernceDateForCalculatingFirstRepaymentDate,
repaymentEvery, frequency);
final LocalDate minimumFirstRepaymentDate = expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
return minimumFirstRepaymentDate.isBefore(derivedFirstRepayment) ? derivedFirstRepayment
:deriveFirstRepaymentDateForJLGLoans(repaymentEvery, expectedDisbursementDate,derivedFirstRepayment, repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
}
private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final LocalDate disbursalDate,
final LocalDate firstRepaymentDate, final Integer minimumDaysBetweenDisbursalAndFirstRepayment){
final LocalDate minimumFirstRepaymentDate = disbursalDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
if(firstRepaymentDate.isBefore(minimumFirstRepaymentDate)){
final String errorMessage = "Number of days between disbursal date (" + disbursalDate + ") and first repayment date ("
+ firstRepaymentDate + ") can't be less than minimumDaysBetweenDisbursalAndFirstRepayment ("
+ minimumDaysBetweenDisbursalAndFirstRepayment + ")." ;
throw new MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException(errorMessage);
}
}
} }
@@ -111,6 +111,19 @@ public final class LoanApplicationCommandFromApiJsonHelper {
if (loanType.isJLGAccount()) { if (loanType.isJLGAccount()) {
baseDataValidator.reset().parameter("clientId").value(clientId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter("clientId").value(clientId).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("groupId").value(groupId).notNull().longGreaterThanZero(); baseDataValidator.reset().parameter("groupId").value(groupId).notNull().longGreaterThanZero();
// if it is JLG loan then must have a meeting details (calendar)
final String calendarIdParameterName = "calendarId";
final Long calendarId = this.fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero();
// if it is JLG loan then must have a syncDisbursement
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
final Boolean syncDisbursement = this.fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element);
if (syncDisbursement == null) {
baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false);
}
} }
} }
@@ -331,25 +344,6 @@ public final class LoanApplicationCommandFromApiJsonHelper {
} }
} }
boolean meetingIdRequired = false;
// validate syncDisbursement
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (this.fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) {
final Boolean syncDisbursement = this.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 || this.fromApiJsonHelper.parameterExists(calendarIdParameterName, element)) {
final Long calendarId = this.fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element);
baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)) { if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)) {
final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName,
element); element);
@@ -59,8 +59,8 @@ public interface LoanProductConstants {
public static final String outstandingLoanBalanceParameterName = "outstandingLoanBalance"; public static final String outstandingLoanBalanceParameterName = "outstandingLoanBalance";
public static final String graceOnArrearsAgeingParameterName = "graceOnArrearsAgeing"; public static final String graceOnArrearsAgeingParameterName = "graceOnArrearsAgeing";
public static final String overdueDaysForNPAParameterName = "overdueDaysForNPA"; public static final String overdueDaysForNPAParameterName = "overdueDaysForNPA";
public static final String minimumDaysBetweenDisbursalAndFirstRepayment = "minimumDaysBetweenDisbursalAndFirstRepayment";
// Interest recalculation related // Interest recalculation related
public static final String isInterestRecalculationEnabledParameterName = "isInterestRecalculationEnabled"; public static final String isInterestRecalculationEnabledParameterName = "isInterestRecalculationEnabled";
@@ -83,6 +83,7 @@ public class LoanProductData {
private final EnumOptionData daysInYearType; private final EnumOptionData daysInYearType;
private final boolean isInterestRecalculationEnabled; private final boolean isInterestRecalculationEnabled;
private final LoanProductInterestRecalculationData interestRecalculationData; private final LoanProductInterestRecalculationData interestRecalculationData;
private final Integer minimumDaysBetweenDisbursalAndFirstRepayment;
// charges // charges
private final Collection<ChargeData> charges; private final Collection<ChargeData> charges;
@@ -186,6 +187,7 @@ public class LoanProductData {
final EnumOptionData daysInYearType = null; final EnumOptionData daysInYearType = null;
final boolean isInterestRecalculationEnabled = false; final boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = null; final LoanProductInterestRecalculationData interestRecalculationData = null;
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -195,7 +197,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData); interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
} }
public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) { public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) {
@@ -248,6 +250,7 @@ public class LoanProductData {
final EnumOptionData daysInYearType = null; final EnumOptionData daysInYearType = null;
final boolean isInterestRecalculationEnabled = false; final boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = null; final LoanProductInterestRecalculationData interestRecalculationData = null;
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -257,7 +260,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData); interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
} }
public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { public static LoanProductData sensibleDefaultsForNewLoanProductCreation() {
@@ -317,6 +320,8 @@ public class LoanProductData {
final boolean isInterestRecalculationEnabled = false; final boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = LoanProductInterestRecalculationData final LoanProductInterestRecalculationData interestRecalculationData = LoanProductInterestRecalculationData
.sensibleDefaultsForNewLoanProductCreation(); .sensibleDefaultsForNewLoanProductCreation();
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -326,7 +331,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId,
principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle,
multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType,
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData); daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
} }
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings, public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -356,7 +361,8 @@ public class LoanProductData {
Collection<LoanProductBorrowerCycleVariationData> numberOfRepaymentVariations, Boolean multiDisburseLoan, Collection<LoanProductBorrowerCycleVariationData> numberOfRepaymentVariations, Boolean multiDisburseLoan,
Integer maxTrancheCount, BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing, Integer maxTrancheCount, BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing,
final Integer overdueDaysForNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final Integer overdueDaysForNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType,
final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationData interestRecalculationData) { final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationData interestRecalculationData,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.shortName = shortName; this.shortName = shortName;
@@ -394,6 +400,7 @@ public class LoanProductData {
this.closeDate = closeDate; this.closeDate = closeDate;
this.status = status; this.status = status;
this.externalId = externalId; this.externalId = externalId;
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
this.chargeOptions = null; this.chargeOptions = null;
this.penaltyOptions = null; this.penaltyOptions = null;
@@ -508,6 +515,7 @@ public class LoanProductData {
this.multiDisburseLoan = productData.multiDisburseLoan; this.multiDisburseLoan = productData.multiDisburseLoan;
this.maxTrancheCount = productData.maxTrancheCount; this.maxTrancheCount = productData.maxTrancheCount;
this.outstandingLoanBalance = productData.outstandingLoanBalance; this.outstandingLoanBalance = productData.outstandingLoanBalance;
this.minimumDaysBetweenDisbursalAndFirstRepayment = productData.minimumDaysBetweenDisbursalAndFirstRepayment;
this.amortizationTypeOptions = amortizationTypeOptions; this.amortizationTypeOptions = amortizationTypeOptions;
this.interestTypeOptions = interestTypeOptions; this.interestTypeOptions = interestTypeOptions;
@@ -122,6 +122,9 @@ public class LoanProduct extends AbstractPersistable<Long> {
@Column(name = "overdue_days_for_npa", nullable = true) @Column(name = "overdue_days_for_npa", nullable = true)
private Integer overdueDaysForNPA; private Integer overdueDaysForNPA;
@Column(name = "min_days_between_disbursal_and_first_repayment", nullable = true)
private Integer minimumDaysBetweenDisbursalAndFirstRepayment;
@LazyCollection(LazyCollectionOption.FALSE) @LazyCollection(LazyCollectionOption.FALSE)
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true) @OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true)
private LoanProductInterestRecalculationDetails productInterestRecalculationDetails; private LoanProductInterestRecalculationDetails productInterestRecalculationDetails;
@@ -164,6 +167,8 @@ public class LoanProduct extends AbstractPersistable<Long> {
final Integer graceOnPrincipalPayment = command.integerValueOfParameterNamed("graceOnPrincipalPayment"); final Integer graceOnPrincipalPayment = command.integerValueOfParameterNamed("graceOnPrincipalPayment");
final Integer graceOnInterestPayment = command.integerValueOfParameterNamed("graceOnInterestPayment"); final Integer graceOnInterestPayment = command.integerValueOfParameterNamed("graceOnInterestPayment");
final Integer graceOnInterestCharged = command.integerValueOfParameterNamed("graceOnInterestCharged"); final Integer graceOnInterestCharged = command.integerValueOfParameterNamed("graceOnInterestCharged");
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = command
.integerValueOfParameterNamed("minimumDaysBetweenDisbursalAndFirstRepayment");
final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule")); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule"));
final boolean includeInBorrowerCycle = command.booleanPrimitiveValueOfParameterNamed("includeInBorrowerCycle"); final boolean includeInBorrowerCycle = command.booleanPrimitiveValueOfParameterNamed("includeInBorrowerCycle");
@@ -213,7 +218,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
graceOnInterestCharged, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle,
startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount, startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount,
outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationSettings); isInterestRecalculationEnabled, interestRecalculationSettings, minimumDaysBetweenDisbursalAndFirstRepayment);
} }
@@ -430,7 +435,8 @@ public class LoanProduct extends AbstractPersistable<Long> {
final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations, final boolean multiDisburseLoan, final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations, final boolean multiDisburseLoan,
final Integer maxTrancheCount, final BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing, final Integer maxTrancheCount, final BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing,
final Integer overdueDaysForNPA, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, final Integer overdueDaysForNPA, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType,
final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationDetails productInterestRecalculationDetails) { final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationDetails productInterestRecalculationDetails,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment) {
this.fund = fund; this.fund = fund;
this.transactionProcessingStrategy = transactionProcessingStrategy; this.transactionProcessingStrategy = transactionProcessingStrategy;
this.name = name.trim(); this.name = name.trim();
@@ -479,6 +485,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
this.loanProducTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); this.loanProducTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance);
this.overdueDaysForNPA = overdueDaysForNPA; this.overdueDaysForNPA = overdueDaysForNPA;
this.productInterestRecalculationDetails = productInterestRecalculationDetails; this.productInterestRecalculationDetails = productInterestRecalculationDetails;
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
} }
public MonetaryCurrency getCurrency() { public MonetaryCurrency getCurrency() {
@@ -650,6 +657,15 @@ public class LoanProduct extends AbstractPersistable<Long> {
this.overdueDaysForNPA = newValue; this.overdueDaysForNPA = newValue;
} }
if (command.isChangeInIntegerParameterNamed(LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment,
this.minimumDaysBetweenDisbursalAndFirstRepayment)) {
final Integer newValue = command
.integerValueOfParameterNamed(LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment);
actualChanges.put(LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment, newValue);
actualChanges.put("locale", localeAsInput);
this.minimumDaysBetweenDisbursalAndFirstRepayment = newValue;
}
/** /**
* Update interest recalculation settings * Update interest recalculation settings
*/ */
@@ -793,6 +809,10 @@ public class LoanProduct extends AbstractPersistable<Long> {
return this.loanProductRelatedDetail.isInterestRecalculationEnabled(); return this.loanProductRelatedDetail.isInterestRecalculationEnabled();
} }
public Integer getMinimumDaysBetweenDisbursalAndFirstRepayment(){
return this.minimumDaysBetweenDisbursalAndFirstRepayment == null?0:this.minimumDaysBetweenDisbursalAndFirstRepayment;
}
public LoanProductBorrowerCycleVariations fetchLoanProductBorrowerCycleVariationById(Long id) { public LoanProductBorrowerCycleVariations fetchLoanProductBorrowerCycleVariationById(Long id) {
LoanProductBorrowerCycleVariations borrowerCycleVariation = null; LoanProductBorrowerCycleVariations borrowerCycleVariation = null;
for (LoanProductBorrowerCycleVariations cycleVariation : this.borrowerCycleVariations) { for (LoanProductBorrowerCycleVariations cycleVariation : this.borrowerCycleVariations) {
@@ -70,7 +70,8 @@ public final class LoanProductDataValidator {
LoanProductConstants.interestRecalculationCompoundingMethodParameterName, LoanProductConstants.interestRecalculationCompoundingMethodParameterName,
LoanProductConstants.recalculationRestFrequencyDateParamName, LoanProductConstants.recalculationRestFrequencyDateParamName,
LoanProductConstants.recalculationRestFrequencyIntervalParameterName, LoanProductConstants.recalculationRestFrequencyIntervalParameterName,
LoanProductConstants.recalculationRestFrequencyTypeParameterName)); LoanProductConstants.recalculationRestFrequencyTypeParameterName,
LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment));
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@@ -104,6 +105,13 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter("fundId").value(fundId).ignoreIfNull().integerGreaterThanZero(); baseDataValidator.reset().parameter("fundId").value(fundId).ignoreIfNull().integerGreaterThanZero();
} }
if (this.fromApiJsonHelper.parameterExists("minimumDaysBetweenDisbursalAndFirstRepayment", element)) {
final Long minimumDaysBetweenDisbursalAndFirstRepayment = this.fromApiJsonHelper.extractLongNamed(
"minimumDaysBetweenDisbursalAndFirstRepayment", element);
baseDataValidator.reset().parameter("minimumDaysBetweenDisbursalAndFirstRepayment")
.value(minimumDaysBetweenDisbursalAndFirstRepayment).ignoreIfNull().integerGreaterThanZero();
}
final Boolean includeInBorrowerCycle = this.fromApiJsonHelper.extractBooleanNamed("includeInBorrowerCycle", element); final Boolean includeInBorrowerCycle = this.fromApiJsonHelper.extractBooleanNamed("includeInBorrowerCycle", element);
baseDataValidator.reset().parameter("includeInBorrowerCycle").value(includeInBorrowerCycle).ignoreIfNull() baseDataValidator.reset().parameter("includeInBorrowerCycle").value(includeInBorrowerCycle).ignoreIfNull()
.validateForBooleanValue(); .validateForBooleanValue();
@@ -120,6 +120,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, lp.interest_calculated_in_period_enum as interestCalculationInPeriodMethod," + "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, lp.interest_calculated_in_period_enum as interestCalculationInPeriodMethod,"
+ "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, lp.min_number_of_repayments as minNumberOfRepayments, lp.max_number_of_repayments as maxNumberOfRepayments, " + "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, lp.min_number_of_repayments as minNumberOfRepayments, lp.max_number_of_repayments as maxNumberOfRepayments, "
+ "lp.grace_on_principal_periods as graceOnPrincipalPayment, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged,lp.grace_on_arrears_ageing as graceOnArrearsAgeing,lp.overdue_days_for_npa as overdueDaysForNPA, " + "lp.grace_on_principal_periods as graceOnPrincipalPayment, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged,lp.grace_on_arrears_ageing as graceOnArrearsAgeing,lp.overdue_days_for_npa as overdueDaysForNPA, "
+ "lp.min_days_between_disbursal_and_first_repayment As minimumDaysBetweenDisbursalAndFirstRepayment, "
+ "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, " + "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, "
+ "lp.accounting_type as accountingType, lp.include_in_borrower_cycle as includeInBorrowerCycle,lp.use_borrower_cycle as useBorrowerCycle, lp.start_date as startDate, lp.close_date as closeDate, " + "lp.accounting_type as accountingType, lp.include_in_borrower_cycle as includeInBorrowerCycle,lp.use_borrower_cycle as useBorrowerCycle, lp.start_date as startDate, lp.close_date as closeDate, "
+ "lp.allow_multiple_disbursals as multiDisburseLoan, lp.max_disbursals as maxTrancheCount, lp.max_outstanding_loan_balance as outstandingLoanBalance, " + "lp.allow_multiple_disbursals as multiDisburseLoan, lp.max_disbursals as maxTrancheCount, lp.max_outstanding_loan_balance as outstandingLoanBalance, "
@@ -172,6 +173,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged"); final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged");
final Integer graceOnArrearsAgeing = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnArrearsAgeing"); final Integer graceOnArrearsAgeing = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnArrearsAgeing");
final Integer overdueDaysForNPA = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "overdueDaysForNPA"); final Integer overdueDaysForNPA = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "overdueDaysForNPA");
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = JdbcSupport.getInteger(rs,"minimumDaysBetweenDisbursalAndFirstRepayment");
final Integer accountingRuleId = JdbcSupport.getInteger(rs, "accountingType"); final Integer accountingRuleId = JdbcSupport.getInteger(rs, "accountingType");
final EnumOptionData accountingRuleType = AccountingEnumerations.accountingRuleType(accountingRuleId); final EnumOptionData accountingRuleType = AccountingEnumerations.accountingRuleType(accountingRuleId);
@@ -263,7 +265,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
closeDate, status, externalId, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, closeDate, status, externalId, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle,
numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData); interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
} }
} }
@@ -0,0 +1,2 @@
ALTER TABLE `m_product_loan`
ADD COLUMN `min_days_between_disbursal_and_first_repayment` INT(3) NULL AFTER `interest_recalculation_enabled`;