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> charges = new ArrayList<HashMap>();
private String recalculationRestFrequencyDate = null;
private String repaymentsStartingFromDate = null;
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("submittedOnDate", this.submittedOnDate);
map.put("loanType", this.loanType);
map.put("repaymentsStartingFromDate", this.repaymentsStartingFromDate);
if (charges != null) {
map.put("charges", charges);
}
@@ -240,4 +243,9 @@ public class LoanApplicationTestBuilder {
this.recalculationRestFrequencyDate = recalculationRestFrequencyDate;
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 recalculationRestFrequencyInterval = "0";
private String recalculationRestFrequencyDate = null;
private String minimumDaysBetweenDisbursalAndFirstRepayment = "0";
public String build(final String chargeId) {
final HashMap<String, Object> map = new HashMap<>();
@@ -120,6 +121,7 @@ public class LoanProductTestBuilder {
map.put("minPrincipal", this.minPrincipal);
map.put("maxPrincipal", this.maxPrincipal);
map.put("overdueDaysForNPA", this.overdueDaysForNPA);
map.put("minimumDaysBetweenDisbursalAndFirstRepayment", this.minimumDaysBetweenDisbursalAndFirstRepayment);
if (multiDisburseLoan) {
map.put("multiDisburseLoan", this.multiDisburseLoan);
map.put("maxTrancheCount", this.maxTrancheCount);
@@ -377,4 +379,8 @@ public class LoanProductTestBuilder {
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");
}
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) {
return Utils.performServerPut(this.requestSpec, this.responseSpec, "/mifosng-provider/api/v1/loans/" + id
+ "?" + Utils.TENANT_IDENTIFIER, loanApplicationJSON, "loanId");
@@ -130,6 +130,10 @@ public class DataValidatorBuilder {
}
/*** 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) {
if (!trueOfFalseFieldProvided && !this.ignoreNullValue) {
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(this.resource).append(".")
@@ -58,7 +58,15 @@ public enum PeriodFrequencyType {
}
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() {
@@ -9,9 +9,9 @@ import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRul
public class LoanDisbursalException extends AbstractPlatformDomainRuleException {
public LoanDisbursalException(final String cureentProduct, final String restrictedProduct) {
super("error.msg.loan.disbursal.failed", "This loan could not be disbursed as `" + cureentProduct + "` and `" + restrictedProduct
+ "` are not allowed to co-exist", new Object[] { cureentProduct, restrictedProduct });
public LoanDisbursalException(final String currentProduct, final String 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[] { currentProduct, restrictedProduct });
}
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.WorkingDaysRepositoryWrapper;
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.CalendarInstance;
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.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
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.DefaultScheduledDateGenerator;
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 repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate;
LocalDate calculatedRepaymentsStartingFromDate = null;
final Boolean synchDisbursement = this.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); }
final String loanTypeParameterName = "loanType";
final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element);
final AccountType loanType = AccountType.fromName(loanTypeStr);
// validate repayment frequency and interval with meeting frequency
// and interval
final PeriodFrequencyType meetingPeriodFrequency = CalendarUtils.getMeetingPeriodFrequencyType(calendar.getRecurrence());
validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType,
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 (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 (calculatedRepaymentsStartingFromDate == 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 = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate,
repaymentEvery, frequency);
} else {// validate user provided repaymentsStartFromDate
/*
* If it is JLG loan, then make sure first repayment falls on meeting
* date
*/
if (loanType.isJLGAccount()) {
if (isRepaymentsStartingFromDateDerived) {
validateRepaymentsStartDateWithMeetingDates(calculatedRepaymentsStartingFromDate, calendar);
} else {
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
final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", 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()) {
baseDataValidator.reset().parameter("clientId").value(clientId).notNull().integerGreaterThanZero();
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)) {
final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName,
element);
@@ -59,8 +59,8 @@ public interface LoanProductConstants {
public static final String outstandingLoanBalanceParameterName = "outstandingLoanBalance";
public static final String graceOnArrearsAgeingParameterName = "graceOnArrearsAgeing";
public static final String overdueDaysForNPAParameterName = "overdueDaysForNPA";
public static final String minimumDaysBetweenDisbursalAndFirstRepayment = "minimumDaysBetweenDisbursalAndFirstRepayment";
// Interest recalculation related
public static final String isInterestRecalculationEnabledParameterName = "isInterestRecalculationEnabled";
@@ -83,6 +83,7 @@ public class LoanProductData {
private final EnumOptionData daysInYearType;
private final boolean isInterestRecalculationEnabled;
private final LoanProductInterestRecalculationData interestRecalculationData;
private final Integer minimumDaysBetweenDisbursalAndFirstRepayment;
// charges
private final Collection<ChargeData> charges;
@@ -186,6 +187,7 @@ public class LoanProductData {
final EnumOptionData daysInYearType = null;
final boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = null;
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -195,7 +197,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData);
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
}
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 boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = null;
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -257,7 +260,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData);
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment);
}
public static LoanProductData sensibleDefaultsForNewLoanProductCreation() {
@@ -317,6 +320,8 @@ public class LoanProductData {
final boolean isInterestRecalculationEnabled = false;
final LoanProductInterestRecalculationData interestRecalculationData = LoanProductInterestRecalculationData
.sensibleDefaultsForNewLoanProductCreation();
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -326,7 +331,7 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId,
principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle,
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,
@@ -356,7 +361,8 @@ public class LoanProductData {
Collection<LoanProductBorrowerCycleVariationData> numberOfRepaymentVariations, Boolean multiDisburseLoan,
Integer maxTrancheCount, BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing,
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.name = name;
this.shortName = shortName;
@@ -394,6 +400,7 @@ public class LoanProductData {
this.closeDate = closeDate;
this.status = status;
this.externalId = externalId;
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
this.chargeOptions = null;
this.penaltyOptions = null;
@@ -508,6 +515,7 @@ public class LoanProductData {
this.multiDisburseLoan = productData.multiDisburseLoan;
this.maxTrancheCount = productData.maxTrancheCount;
this.outstandingLoanBalance = productData.outstandingLoanBalance;
this.minimumDaysBetweenDisbursalAndFirstRepayment = productData.minimumDaysBetweenDisbursalAndFirstRepayment;
this.amortizationTypeOptions = amortizationTypeOptions;
this.interestTypeOptions = interestTypeOptions;
@@ -121,6 +121,9 @@ public class LoanProduct extends AbstractPersistable<Long> {
@Column(name = "overdue_days_for_npa", nullable = true)
private Integer overdueDaysForNPA;
@Column(name = "min_days_between_disbursal_and_first_repayment", nullable = true)
private Integer minimumDaysBetweenDisbursalAndFirstRepayment;
@LazyCollection(LazyCollectionOption.FALSE)
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true)
@@ -164,6 +167,8 @@ public class LoanProduct extends AbstractPersistable<Long> {
final Integer graceOnPrincipalPayment = command.integerValueOfParameterNamed("graceOnPrincipalPayment");
final Integer graceOnInterestPayment = command.integerValueOfParameterNamed("graceOnInterestPayment");
final Integer graceOnInterestCharged = command.integerValueOfParameterNamed("graceOnInterestCharged");
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = command
.integerValueOfParameterNamed("minimumDaysBetweenDisbursalAndFirstRepayment");
final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule"));
final boolean includeInBorrowerCycle = command.booleanPrimitiveValueOfParameterNamed("includeInBorrowerCycle");
@@ -213,7 +218,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
graceOnInterestCharged, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle,
startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount,
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 Integer maxTrancheCount, final BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing,
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.transactionProcessingStrategy = transactionProcessingStrategy;
this.name = name.trim();
@@ -479,6 +485,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
this.loanProducTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance);
this.overdueDaysForNPA = overdueDaysForNPA;
this.productInterestRecalculationDetails = productInterestRecalculationDetails;
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
}
public MonetaryCurrency getCurrency() {
@@ -650,6 +657,15 @@ public class LoanProduct extends AbstractPersistable<Long> {
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
*/
@@ -793,6 +809,10 @@ public class LoanProduct extends AbstractPersistable<Long> {
return this.loanProductRelatedDetail.isInterestRecalculationEnabled();
}
public Integer getMinimumDaysBetweenDisbursalAndFirstRepayment(){
return this.minimumDaysBetweenDisbursalAndFirstRepayment == null?0:this.minimumDaysBetweenDisbursalAndFirstRepayment;
}
public LoanProductBorrowerCycleVariations fetchLoanProductBorrowerCycleVariationById(Long id) {
LoanProductBorrowerCycleVariations borrowerCycleVariation = null;
for (LoanProductBorrowerCycleVariations cycleVariation : this.borrowerCycleVariations) {
@@ -70,7 +70,8 @@ public final class LoanProductDataValidator {
LoanProductConstants.interestRecalculationCompoundingMethodParameterName,
LoanProductConstants.recalculationRestFrequencyDateParamName,
LoanProductConstants.recalculationRestFrequencyIntervalParameterName,
LoanProductConstants.recalculationRestFrequencyTypeParameterName));
LoanProductConstants.recalculationRestFrequencyTypeParameterName,
LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment));
private final FromJsonHelper fromApiJsonHelper;
@@ -103,7 +104,14 @@ public final class LoanProductDataValidator {
final Long fundId = this.fromApiJsonHelper.extractLongNamed("fundId", element);
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);
baseDataValidator.reset().parameter("includeInBorrowerCycle").value(includeInBorrowerCycle).ignoreIfNull()
.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.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.min_days_between_disbursal_and_first_repayment As minimumDaysBetweenDisbursalAndFirstRepayment, "
+ "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.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 graceOnArrearsAgeing = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnArrearsAgeing");
final Integer overdueDaysForNPA = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "overdueDaysForNPA");
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = JdbcSupport.getInteger(rs,"minimumDaysBetweenDisbursalAndFirstRepayment");
final Integer accountingRuleId = JdbcSupport.getInteger(rs, "accountingType");
final EnumOptionData accountingRuleType = AccountingEnumerations.accountingRuleType(accountingRuleId);
@@ -263,7 +265,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
closeDate, status, externalId, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle,
numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
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`;