From 1c5364ac7f04a8458788d28918a2faade23f32b7 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 24 Oct 2014 16:08:30 +0530 Subject: [PATCH] MIFOSX-1584 Added business logic for minimum number of days between disbursal day and first repayment date --- ...BetweenDisbursalAndFirstRepaymentTest.java | 201 ++++++++++++++++++ .../common/CalendarHelper.java | 61 ++++++ .../loans/LoanApplicationTestBuilder.java | 8 + .../common/loans/LoanProductTestBuilder.java | 6 + .../common/loans/LoanTransactionHelper.java | 4 + .../core/data/DataValidatorBuilder.java | 4 + .../common/domain/PeriodFrequencyType.java | 10 +- .../exception/LoanDisbursalException.java | 6 +- ...sbursalAndFirstRepaymentRuleException.java | 22 ++ .../service/LoanScheduleAssembler.java | 171 ++++++++++++--- ...anApplicationCommandFromApiJsonHelper.java | 32 ++- .../loanproduct/LoanProductConstants.java | 2 +- .../loanproduct/data/LoanProductData.java | 16 +- .../loanproduct/domain/LoanProduct.java | 24 ++- .../LoanProductDataValidator.java | 12 +- .../LoanProductReadPlatformServiceImpl.java | 4 +- ..._between_disbursal_and_first_repayment.sql | 2 + 17 files changed, 525 insertions(+), 60 deletions(-) create mode 100644 mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/MinimumDaysBetweenDisbursalAndFirstRepaymentTest.java create mode 100644 mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/CalendarHelper.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException.java create mode 100644 mifosng-provider/src/main/resources/sql/migrations/core_db/V207__minimum_days_between_disbursal_and_first_repayment.sql diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/MinimumDaysBetweenDisbursalAndFirstRepaymentTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/MinimumDaysBetweenDisbursalAndFirstRepaymentTest.java new file mode 100644 index 000000000..fbeb274b5 --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/MinimumDaysBetweenDisbursalAndFirstRepaymentTest.java @@ -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 error = (List) 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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/CalendarHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/CalendarHelper.java new file mode 100644 index 000000000..8f5c7be62 --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/CalendarHelper.java @@ -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 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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java index d4cabcded..e90eb8853 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -47,6 +47,7 @@ public class LoanApplicationTestBuilder { private List disbursementData = null; private List charges = new ArrayList(); 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; + } } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java index 1a494290a..113a5da30 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanProductTestBuilder.java @@ -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 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; + } } diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java index 207d4e3ce..7aeb0db61 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java @@ -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"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index cec01f0b3..ea35cf403 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -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(".") diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/PeriodFrequencyType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/PeriodFrequencyType.java index 70a160022..b305c48af 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/PeriodFrequencyType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/common/domain/PeriodFrequencyType.java @@ -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() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/LoanDisbursalException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/LoanDisbursalException.java index f16e6a9b0..82bc324d1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/LoanDisbursalException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/LoanDisbursalException.java @@ -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) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException.java new file mode 100644 index 000000000..b4c9e3682 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MinimumDaysBetweenDisbursalAndFirstRepaymentRuleException.java @@ -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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 73337ecdb..6e5b4b8b8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -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); + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index ee2d9c7af..6e2dd586d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java index bfe23d0a7..ee172aeb3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/LoanProductConstants.java @@ -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"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java index 4ce3867c6..8726cc806 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java @@ -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 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 accountingMappings, @@ -356,7 +361,8 @@ public class LoanProductData { Collection 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; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java index b09f0cb47..1718e22cb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java @@ -121,6 +121,9 @@ public class LoanProduct extends AbstractPersistable { @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 { 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 { 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 { final Set 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 { 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 { 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 { 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) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 3adebd408..5f42baf33 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -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(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index ac781d76b..817fa475d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -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); } } diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__minimum_days_between_disbursal_and_first_repayment.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__minimum_days_between_disbursal_and_first_repayment.sql new file mode 100644 index 000000000..7b89658b0 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__minimum_days_between_disbursal_and_first_repayment.sql @@ -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`; \ No newline at end of file