From c548f501dedf3e65bc04bc17caba45f81d689a05 Mon Sep 17 00:00:00 2001 From: Pramod Nuthakki Date: Mon, 17 Feb 2014 09:20:47 -0800 Subject: [PATCH] MIFOSX-92 Tranche loan functionality --- api-docs/apiLive.htm | 40 +- .../core_db/V146__tranche_loans.sql | 38 + .../commands/domain/CommandWrapper.java | 9 +- .../service/CommandWrapperBuilder.java | 10 + .../SynchronousCommandProcessingService.java | 8 +- .../loanaccount/api/LoanApiConstants.java | 26 + .../LoanDisbursementDetailApiResource.java | 95 +++ .../loanaccount/api/LoansApiResource.java | 23 +- .../loanaccount/data/DisbursementData.java | 34 +- .../loanaccount/data/LoanAccountData.java | 127 +++- .../data/LoanApplicationTimelineData.java | 4 +- .../data/LoanTermVariationsData.java | 38 + .../loanaccount/data/LoanTransactionData.java | 11 +- .../RepaymentScheduleRelatedLoanData.java | 6 +- .../portfolio/loanaccount/domain/Loan.java | 661 ++++++++++++++++-- .../loanaccount/domain/LoanCharge.java | 9 +- .../domain/LoanDisbursementDetails.java | 129 ++++ .../loanaccount/domain/LoanEvent.java | 3 +- ...oanRepaymentScheduleProcessingWrapper.java | 16 +- .../loanaccount/domain/LoanSummary.java | 5 + .../domain/LoanTermVariationType.java | 38 + .../domain/LoanTermVariations.java | 74 ++ .../loanaccount/domain/LoanTransaction.java | 2 +- .../ExceedingTrancheCountException.java | 11 + .../exception/LoanDisbursalException.java | 3 + ...ultiDisbursementDataRequiredException.java | 11 + .../UpdateLoanDisbuseDateCommandHandler.java | 32 + .../data/LoanSchedulePeriodData.java | 2 +- .../domain/AbstractLoanScheduleGenerator.java | 137 +++- .../domain/DefaultScheduledDateGenerator.java | 65 +- .../domain/FinanicalFunctions.java | 12 + .../domain/LoanApplicationTerms.java | 90 ++- .../LoanScheduleModelDisbursementPeriod.java | 23 +- .../domain/LoanScheduleModelPeriod.java | 2 + .../LoanScheduleModelRepaymentPeriod.java | 15 +- .../domain/ScheduledDateGenerator.java | 9 +- ...DisbursementDisbursementDateException.java | 11 + .../MultiDisbursementEmiAmountException.java | 11 + ...DisbursementOutstandingAmoutException.java | 11 + .../service/LoanScheduleAssembler.java | 54 +- ...ateLoanScheduleQueryFromApiJsonHelper.java | 4 +- ...anApplicationCommandFromApiJsonHelper.java | 90 ++- .../LoanEventApiJsonValidator.java | 35 +- .../loanaccount/service/LoanAssembler.java | 80 ++- .../service/LoanReadPlatformService.java | 10 +- .../service/LoanReadPlatformServiceImpl.java | 138 +++- .../service/LoanWritePlatformService.java | 2 + ...WritePlatformServiceJpaRepositoryImpl.java | 185 ++++- .../loanproduct/LoanProductConstants.java | 5 + .../loanproduct/data/LoanProductData.java | 42 +- .../loanproduct/domain/LoanProduct.java | 33 +- .../domain/LoanProductRelatedDetail.java | 5 +- .../domain/LoanProductTrancheDetails.java | 77 ++ .../LoanProductDataValidator.java | 34 +- .../loanproduct/service/LoanEnumerations.java | 21 + .../LoanProductReadPlatformServiceImpl.java | 8 +- 56 files changed, 2405 insertions(+), 269 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V146__tranche_loans.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanDisbursementDetailApiResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanDisbursementDetails.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/ExceedingTrancheCountException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MultiDisbursementDataRequiredException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UpdateLoanDisbuseDateCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementDisbursementDateException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementEmiAmountException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementOutstandingAmoutException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductTrancheDetails.java diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index b6131856e..96ede4560 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -5409,7 +5409,11 @@ GET https://DomainName/api/v1/loans/{loanId} "linkedAccount":{ "id":1, "accountNo":"000000001" - } + }, + "disbursementDetails":[{"id":71,"expectedDisbursementDate":[2013,11,1],"principal":22000.000000,"approvedPrincipal":22000.000000}], + "fixedEmiAmount":1100.000000, + "maxOutstandingLoanBalance":35000, + "canDisburse":false } } @@ -5834,7 +5838,9 @@ Request Body:
Optional Fields
- graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged,linkAccountId + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged,linkAccountId, + fixedEmiAmount,maxOutstandingLoanBalance,disbursementData + @@ -5864,8 +5870,12 @@ Request Body: "interestCalculationPeriodType": 1, "transactionProcessingStrategyId": 1, "expectedDisbursementDate": "10 Jun 2013", - "submittedOnDate": "10 Jun 2013" - "linkAccountId" : "1" + "submittedOnDate": "10 Jun 2013", + "linkAccountId" : "1", + "fixedEmiAmount":1100, + "maxOutstandingLoanBalance":"35000", + "disbursementData":[{"expectedDisbursementDate":"01 November 2013", + "principal":22000,"approvedPrincipal":22000}] } @@ -6109,6 +6119,15 @@ Request Body: actualDisbursementDate +
+ + + + + + + +
Optional Fields
transactionAmount,fixedEmiAmount
@@ -6121,6 +6140,8 @@ Request Body: { "dateFormat": "dd MMMM yyyy", "locale": "en", + "transactionAmount":10000, + "fixedEmiAmount""1100, "actualDisbursementDate": "14 May 2013", "paymentTypeId": "12", "note": "", @@ -6158,7 +6179,8 @@ Request Body: }, "locale": "en", "dateFormat": "dd MMMM yyyy", - "actualDisbursementDate": "14 May 2013" + "actualDisbursementDate": "14 May 2013", + "transactionAmount":10000 } } @@ -9153,7 +9175,8 @@ GET https://DomainName/api/v1/loanproducts/{productId} "name": "income from penalities 2", "glCode": "11" } - } + }, + "multiDisburseLoan":true,"maxTrancheCount":3,"outstandingLoanBalance":36000.000000 ] } @@ -9193,7 +9216,10 @@ GET https://DomainName/api/v1/loanproducts/{productId} inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, - interestRateVariationsForBorrowerCycle + interestRateVariationsForBorrowerCycle, + multiDisburseLoan,maxTrancheCount, + outstandingLoanBalance + diff --git a/mifosng-db/migrations/core_db/V146__tranche_loans.sql b/mifosng-db/migrations/core_db/V146__tranche_loans.sql new file mode 100644 index 000000000..d009bce56 --- /dev/null +++ b/mifosng-db/migrations/core_db/V146__tranche_loans.sql @@ -0,0 +1,38 @@ +ALTER TABLE `m_product_loan` + ADD COLUMN `multi_disburse_loan` TINYINT(1) NOT NULL DEFAULT '0' AFTER `close_date`, + ADD COLUMN `max_tranche_count` INT(2) NULL DEFAULT NULL AFTER `multi_disburse_loan`, + ADD COLUMN `outstanding_loan_balance` DECIMAL(19,6) NULL DEFAULT NULL AFTER `max_tranche_count`; + +CREATE TABLE `m_loan_disbursement_detail` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_id` BIGINT(20) NOT NULL, + `expected_disburse_date` DATETIME NOT NULL, + `disbursedon_date` DATETIME NULL, + `principal` DECIMAL(19,6) NOT NULL, + `approved_principal` DECIMAL(19,6) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_loan_disbursement_detail_loan_id` FOREIGN KEY (`loan_id`) REFERENCES `m_loan` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + +ALTER TABLE `m_loan` + ADD COLUMN `fixed_emi_amount` DECIMAL(19,6) NULL AFTER `loan_product_counter`, + ADD COLUMN `approved_principal` DECIMAL(19,6) NOT NULL AFTER `principal_amount`, + ADD COLUMN `max_outstanding_loan_balance` DECIMAL(19,6) NULL DEFAULT NULL AFTER `fixed_emi_amount`; + +UPDATE m_loan ml SET ml.approved_principal = ml.principal_amount; + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES ('portfolio', 'UPDATE_DISBURSEMENTDETAIL', 'DISBURSEMENTDETAIL', 'UPDATE', 0); + +CREATE TABLE `m_loan_term_variations` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_id` BIGINT(20) NOT NULL, + `term_type` SMALLINT(2) NOT NULL, + `applicable_from` DATE NOT NULL, + `term_value` DECIMAL(19,6) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_loan_id_m_loan_id` FOREIGN KEY (`loan_id`) REFERENCES `m_loan` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index 82d3f8ff8..d9278f271 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -329,7 +329,10 @@ public class CommandWrapper { public boolean isLoanChargeResource() { return this.entityName.equalsIgnoreCase("LOANCHARGE"); } - + public boolean isLoanDisburseDetailResource() { + return this.entityName.equalsIgnoreCase("DISBURSEMENTDETAIL"); + } + public boolean isCollateralResource() { return this.entityName.equalsIgnoreCase("COLLATERAL"); } @@ -381,6 +384,10 @@ public class CommandWrapper { public boolean isUndoLoanWriteOff() { return this.actionName.equalsIgnoreCase("UNDOWRITEOFF") && this.entityName.equalsIgnoreCase("LOAN"); } + + public boolean isUpdateDisbursementDate() { + return this.actionName.equalsIgnoreCase("UPDATE") && this.entityName.equalsIgnoreCase("DISBURSEMENTDETAIL"); + } public boolean isCloseLoanAsObligationsMet() { return this.actionName.equalsIgnoreCase("CLOSE") && this.entityName.equalsIgnoreCase("LOAN"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 1e1aa0127..bcac311f6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -629,6 +629,16 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId; return this; } + + public CommandWrapperBuilder updateDisbusementDate(final Long loanId,final Long disbursementId) { + this.actionName = "UPDATE"; + this.entityName = "DISBURSEMENTDETAIL"; + this.entityId = disbursementId; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/disbursementdetail/" + disbursementId; + return this; + } + public CommandWrapperBuilder deleteLoanApplication(final Long loanId) { this.actionName = "DELETE"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index 4dc7901bf..4d4c20649 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -348,7 +348,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer handler = this.applicationContext.getBean("loanApplicationDeletionCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUndoLoanWriteOff()) { handler = this.applicationContext.getBean("undoWriteOffLoanCommandHandler", NewCommandSourceHandler.class); - }else { + } else { throw new UnsupportedCommandException(wrapper.commandName()); } } else if (wrapper.isLoanChargeResource()) { @@ -363,7 +363,11 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer } else if (wrapper.isPayLoanCharge()) { handler = this.applicationContext.getBean("payLoanChargeCommandHandler", NewCommandSourceHandler.class); } - } else if (wrapper.isGLAccountResource()) { + } else if(wrapper.isLoanDisburseDetailResource()){ + if(wrapper.isUpdateDisbursementDate()){ + handler = this.applicationContext.getBean("updateLoanDisbuseDateCommandHandler", NewCommandSourceHandler.class); + } + }else if (wrapper.isGLAccountResource()) { if (wrapper.isCreate()) { handler = this.applicationContext.getBean("createGLAccountCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUpdate()) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java new file mode 100644 index 000000000..97c8e2039 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanApiConstants.java @@ -0,0 +1,26 @@ +package org.mifosplatform.portfolio.loanaccount.api; + +public interface LoanApiConstants { + + public static final String emiAmountParameterName = "fixedEmiAmount"; + + public static final String maxOutstandingBalanceParameterName = "maxOutstandingLoanBalance"; + + public static final String disbursementDataParameterName = "disbursementData"; + + public static final String disbursementDateParameterName = "expectedDisbursementDate"; + + public static final String disbursementPrincipalParameterName = "principal"; + + public static final String DISBURSEMENT_DATE_START_WITH_ERROR = "first.disbursement.date.must.start.with.expected.disbursement.date"; + + public static final String PRINCIPAL_AMOUNT_SHOULD_BE_SAME = "sum.of.multi.disburse.amounts.must.equal.with.total.principal"; + + public static final String DISBURSEMENT_DATE_UNIQUE_ERROR = "disbursement.date.must.be.unique.for.tranches"; + + public static final String disbursementIdParameterName = "id"; + + public static final String principalDisbursedParameterName = "transactionAmount"; + + public static final String ALREADY_DISBURSED = "can.not.change.disbursement.date"; +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanDisbursementDetailApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanDisbursementDetailApiResource.java new file mode 100644 index 000000000..0a1f8b0fb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanDisbursementDetailApiResource.java @@ -0,0 +1,95 @@ +/** + * 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.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.commands.domain.CommandWrapper; +import org.mifosplatform.commands.service.CommandWrapperBuilder; +import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; +import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/loans/{loanId}/disbursements") +@Component +@Scope("singleton") +public class LoanDisbursementDetailApiResource { + + private final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "expectedDisbursementDate","actualDisbursementDate", + "principal","approvedPrincipal")); + + private final String resourceNameForPermissions = "LOAN"; + + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final PlatformSecurityContext context; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final LoanReadPlatformService loanReadPlatformService; + + @Autowired + public LoanDisbursementDetailApiResource(final DefaultToApiJsonSerializer toApiJsonSerializer, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,final PlatformSecurityContext context, + final ApiRequestParameterHelper apiRequestParameterHelper, final LoanReadPlatformService loanReadPlatformService) { + this.toApiJsonSerializer = toApiJsonSerializer; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.context = context; + this.loanReadPlatformService = loanReadPlatformService; + } + + @PUT + @Path("{disbursementId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateDisbursementDate(@PathParam("loanId") final Long loanId, @PathParam("disbursementId") final Long disbursementId, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateDisbusementDate(loanId,disbursementId).withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + + @GET + @Path("{disbursementId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retriveDetail(@PathParam("loanId") final Long loanId,@PathParam("disbursementId") final Long disbursementId, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final DisbursementData disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetail(loanId, disbursementId); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, disbursementData, this.RESPONSE_DATA_PARAMETERS); + } + + + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index e98acf008..ae1634a28 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -58,10 +58,13 @@ import org.mifosplatform.portfolio.fund.service.FundReadPlatformService; import org.mifosplatform.portfolio.group.data.GroupGeneralData; import org.mifosplatform.portfolio.group.service.GroupReadPlatformService; import org.mifosplatform.portfolio.group.service.SearchParameters; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; import org.mifosplatform.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException; import org.mifosplatform.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException; import org.mifosplatform.portfolio.loanaccount.guarantor.data.GuarantorData; @@ -281,6 +284,8 @@ public class LoansApiResource { CalendarData meeting = null; Collection notes = null; PortfolioAccountData linkedAccount = null; + Collection disbursementData = null; + Collection emiAmountVariations = null; final Set mandatoryResponseParameters = new HashSet(); final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); @@ -288,7 +293,7 @@ public class LoansApiResource { if (associationParameters.contains("all")) { associationParameters.addAll(Arrays.asList("repaymentSchedule", "transactions", "charges", "guarantors", "collateral", - "notes", "linkedAccount")); + "notes", "linkedAccount","multiDisburseDetails")); } if (associationParameters.contains("guarantors")) { @@ -306,12 +311,23 @@ public class LoansApiResource { loanRepayments = currentLoanRepayments; } } + + if(associationParameters.contains("multiDisburseDetails") || associationParameters.contains("repaymentSchedule")){ + mandatoryResponseParameters.add("multiDisburseDetails"); + disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(loanId); + } + + if(associationParameters.contains("emiAmountVariations") || associationParameters.contains("repaymentSchedule")){ + mandatoryResponseParameters.add("emiAmountVariations"); + emiAmountVariations = this.loanReadPlatformService.retrieveLoanTermVariations(loanId, LoanTermVariationType.EMI_AMOUNT.getValue()); + } + if (associationParameters.contains("repaymentSchedule")) { mandatoryResponseParameters.add("repaymentSchedule"); final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = loanBasicDetails.repaymentScheduleRelatedData(); - repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(loanId, repaymentScheduleRelatedData); + repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(loanId, repaymentScheduleRelatedData, disbursementData); } if (associationParameters.contains("charges")) { @@ -347,6 +363,7 @@ public class LoansApiResource { mandatoryResponseParameters.add("linkedAccount"); linkedAccount = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loanId); } + } Collection productOptions = null; @@ -411,7 +428,7 @@ public class LoansApiResource { charges, collateral, guarantors, meeting, productOptions, loanTermFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions, - loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount); + loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData,emiAmountVariations); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/DisbursementData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/DisbursementData.java index 74593e6e8..624e48804 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/DisbursementData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/DisbursementData.java @@ -12,17 +12,25 @@ import org.joda.time.LocalDate; /** * Immutable data object representing disbursement information. */ -public class DisbursementData { +public class DisbursementData implements Comparable{ + @SuppressWarnings("unused") + private final Long id; private final LocalDate expectedDisbursementDate; private final LocalDate actualDisbursementDate; private final BigDecimal principal; + @SuppressWarnings("unused") + private final BigDecimal approvedPrincipal; + - public DisbursementData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate, - final BigDecimal principalDisbursed) { + public DisbursementData(Long id, final LocalDate expectedDisbursementDate, + final LocalDate actualDisbursementDate, final BigDecimal principalDisbursed, + final BigDecimal approvedPrincipal) { + this.id = id; this.expectedDisbursementDate = expectedDisbursementDate; this.actualDisbursementDate = actualDisbursementDate; this.principal = principalDisbursed; + this.approvedPrincipal = approvedPrincipal; } public LocalDate disbursementDate() { @@ -40,4 +48,24 @@ public class DisbursementData { public boolean isDisbursed() { return this.actualDisbursementDate != null; } + + @Override + public int compareTo(final DisbursementData obj) { + if (obj == null) { return -1; } + + return obj.expectedDisbursementDate.compareTo(this.expectedDisbursementDate); + } + + public boolean isDueForDisbursement(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive) { + final LocalDate dueDate = disbursementDate(); + return occursOnDayFromAndUpToAndIncluding(fromNotInclusive, upToAndInclusive, dueDate); + } + + private boolean occursOnDayFromAndUpToAndIncluding(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive, + final LocalDate target) { + return target != null && target.isAfter(fromNotInclusive) && !target.isAfter(upToAndInclusive); + } + + + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index 0ac414e20..0802052fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -27,6 +27,7 @@ import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.collateral.data.CollateralData; import org.mifosplatform.portfolio.fund.data.FundData; import org.mifosplatform.portfolio.group.data.GroupGeneralData; +import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.guarantor.data.GuarantorData; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.mifosplatform.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; @@ -71,6 +72,7 @@ public class LoanAccountData { // terms private final CurrencyData currency; private final BigDecimal principal; + private final BigDecimal approvedPrincipal; private final Integer termFrequency; private final EnumOptionData termPeriodFrequencyType; private final Integer numberOfRepayments; @@ -108,7 +110,7 @@ public class LoanAccountData { private final Collection guarantors; private final CalendarData meeting; private final Collection notes; - + private final Collection disbursementDetails; // template private final Collection productOptions; private final Collection loanOfficerOptions; @@ -136,7 +138,17 @@ public class LoanAccountData { // linkable account details private final PortfolioAccountData linkedAccount; private final Collection accountLinkingOptions; - + + private final Boolean multiDisburseLoan; + + private final BigDecimal fixedEmiAmount; + + private final BigDecimal maxOutstandingLoanBalance; + + private final Boolean canDisburse; + + private final Collection emiAmountVariations; + /** * Used to produce a {@link LoanAccountData} with only collateral options * for now. @@ -212,9 +224,14 @@ public class LoanAccountData { final Integer loanProductCounter = null; final Collection accountLinkingOptions = null; final PortfolioAccountData linkedAccount = null; + final Collection disbursementData = null; + final Boolean multiDisburseLoan = null; + final BigDecimal fixedEmiAmount = null; + final BigDecimal maxOutstandingLoanBalance = null; + final Collection emiAmountVariations = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + currencyData, principal,principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, @@ -223,7 +240,8 @@ public class LoanAccountData { repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, - syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount); + syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementData, + multiDisburseLoan, fixedEmiAmount,maxOutstandingLoanBalance,emiAmountVariations); } /** @@ -301,10 +319,15 @@ public class LoanAccountData { final Integer loanProductCounter = null; final Collection accountLinkingOptions = null; final PortfolioAccountData linkedAccount = null; + final Collection disbursementData = null; + final Boolean multiDisburseLoan = null; + final BigDecimal fixedEmiAmount = null; + final BigDecimal maxOutstandingLoanBalance = null; + final Collection emiAmountVariations = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + currencyData, principal,principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, @@ -313,7 +336,8 @@ public class LoanAccountData { repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter, - notes, accountLinkingOptions, linkedAccount); + notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingLoanBalance, + emiAmountVariations); } public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { @@ -321,7 +345,7 @@ public class LoanAccountData { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, clientAcc.clientId, clientAcc.clientName, clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, - acc.principal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, + acc.principal,acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, @@ -332,7 +356,8 @@ public class LoanAccountData { acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount); + acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount,acc.maxOutstandingLoanBalance, + acc.emiAmountVariations); } /** @@ -411,10 +436,15 @@ public class LoanAccountData { final Integer loanProductCounter = null; final Collection accountLinkingOptions = null; final PortfolioAccountData linkedAccount = null; + final Collection disbursementData = null; + final Boolean multiDisburseLoan = null; + final BigDecimal fixedEmiAmount = null; + final BigDecimal maxOutstandingBalance = null; + final Collection emiAmountVariations = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + currencyData, principal,principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, transactionProcessingStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, @@ -423,14 +453,15 @@ public class LoanAccountData { repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter, - notes, accountLinkingOptions, linkedAccount); + notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount,maxOutstandingBalance, + emiAmountVariations); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal,acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, @@ -442,7 +473,8 @@ public class LoanAccountData { acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount); + acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount,acc.maxOutstandingLoanBalance, + acc.emiAmountVariations); } public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, @@ -514,6 +546,8 @@ public class LoanAccountData { final Integer loanProductCounter = null; final PortfolioAccountData linkedAccount = null; final Collection accountLinkingOptions = null; + final Collection disbursementData = null; + final Collection emiAmountVariations = null; BigDecimal principal = null; BigDecimal interestRatePerPeriod = null; Integer numberOfRepayments = null; @@ -542,11 +576,11 @@ public class LoanAccountData { } final Integer termFrequency = numberOfRepayments * product.getRepaymentEvery(); - + final BigDecimal fixedEmi = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), loanPurposeId, - loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), principal, totalOverpaid, + loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), principal,principal, totalOverpaid, product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, numberOfRepayments, product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), transactionProcessingStrategyName, product.getAmortizationType(), interestRatePerPeriod, product.getInterestRateFrequencyType(), @@ -557,7 +591,8 @@ public class LoanAccountData { repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter, - notes, accountLinkingOptions, linkedAccount); + notes, accountLinkingOptions, linkedAccount, disbursementData, product.getMultiDisburseLoan(), fixedEmi, product.getOutstandingLoanBalance(), + emiAmountVariations); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -595,7 +630,7 @@ public class LoanAccountData { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, - product.getCurrency(), product.getPrincipal(), acc.totalOverpaid, product.getInArrearsTolerance(), termFrequency, + product.getCurrency(), product.getPrincipal(),product.getPrincipal(), acc.totalOverpaid, product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), product.getTransactionProcessingStrategyName(), product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), @@ -607,7 +642,8 @@ public class LoanAccountData { interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, acc.accountLinkingOptions, - acc.linkedAccount); + acc.linkedAccount, acc.disbursementDetails, product.getMultiDisburseLoan(), acc.fixedEmiAmount,product.getOutstandingLoanBalance(), + acc.emiAmountVariations); } /* @@ -618,8 +654,9 @@ public class LoanAccountData { final String externalId, final Long clientId, final String clientName, final Long clientOfficeId, final GroupGeneralData group, final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, final Long fundId, final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, - final String loanOfficerName, final CurrencyData currencyData, final BigDecimal principal, final BigDecimal totalOverpaid, - final BigDecimal inArrearsTolerance, final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, + final String loanOfficerName, final CurrencyData currencyData, final BigDecimal principal,final BigDecimal approvedPrincipal, + final BigDecimal totalOverpaid, final BigDecimal inArrearsTolerance, + final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final EnumOptionData repaymentFrequencyType, final Long transactionStrategyId, final String transactionStrategyName, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, final BigDecimal annualInterestRate, @@ -627,7 +664,8 @@ public class LoanAccountData { final LocalDate expectedFirstRepaymentOnDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, final BigDecimal feeChargesDueAtDisbursementCharged, - final Boolean syncDisbursementWithMeeting, final Integer loancounter, final Integer loanProductCounter) { + final Boolean syncDisbursementWithMeeting, final Integer loancounter, final Integer loanProductCounter, + final Boolean multiDisburseLoan,final BigDecimal fixedEmiAmont , final BigDecimal outstandingLoanBalance) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -653,10 +691,12 @@ public class LoanAccountData { final Collection calendarOptions = null; final Collection accountLinkingOptions = null; final PortfolioAccountData linkedAccount = null; - + final Collection disbursementData= null; + final Collection emiAmountVariations= null; + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + currencyData, principal,approvedPrincipal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, transactionStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, @@ -665,7 +705,7 @@ public class LoanAccountData { repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, - accountLinkingOptions, linkedAccount); + accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmont, outstandingLoanBalance,emiAmountVariations); } /* @@ -684,10 +724,11 @@ public class LoanAccountData { final Collection loanOfficerOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions, final Collection calendarOptions, final Collection notes, final Collection accountLinkingOptions, - final PortfolioAccountData linkedAccount) { + final PortfolioAccountData linkedAccount,final Collection disbursementDetails, + final Collection emiAmountVariations) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal,acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, @@ -697,7 +738,8 @@ public class LoanAccountData { productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, - acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, accountLinkingOptions, linkedAccount); + acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, notes, accountLinkingOptions, linkedAccount, disbursementDetails, + acc.multiDisburseLoan, acc.fixedEmiAmount,acc.maxOutstandingLoanBalance,emiAmountVariations); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -708,13 +750,13 @@ public class LoanAccountData { acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions, - acc.linkedAccount); + acc.linkedAccount, acc.disbursementDetails,acc.emiAmountVariations); } public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal,acc.approvedPrincipal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.transactionProcessingStrategyName, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, @@ -726,7 +768,8 @@ public class LoanAccountData { acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting, acc.loanCounter, acc.loanProductCounter, acc.notes, acc.accountLinkingOptions, - acc.linkedAccount); + acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount,acc.maxOutstandingLoanBalance, + acc.emiAmountVariations); } private LoanAccountData( @@ -750,6 +793,7 @@ public class LoanAccountData { final String loanOfficerName, // final CurrencyData currency, final BigDecimal principal, + final BigDecimal approvedPrincipal, final BigDecimal totalOverpaid, final BigDecimal inArrearsTolerance, // final Integer termFrequency, @@ -775,7 +819,8 @@ public class LoanAccountData { final Collection loanCollateralOptions, final Collection calendarOptions, final Boolean syncDisbursementWithMeeting, final Integer loanCounter, final Integer loanProductCounter, final Collection notes, final Collection accountLinkingOptions, - final PortfolioAccountData linkedAccount) { + final PortfolioAccountData linkedAccount,final Collection disbursementDetails,final Boolean multiDisburseLoan, BigDecimal fixedEmiAmount, + final BigDecimal maxOutstandingLoanBalance,final Collection emiAmountVariations) { this.id = id; this.accountNo = accountNo; this.status = status; @@ -796,6 +841,7 @@ public class LoanAccountData { this.loanOfficerName = loanOfficerName; this.currency = currency; this.principal = principal; + this.approvedPrincipal = approvedPrincipal; this.totalOverpaid = totalOverpaid; this.inArrearsTolerance = inArrearsTolerance; this.termFrequency = termFrequency; @@ -888,10 +934,29 @@ public class LoanAccountData { this.linkedAccount = linkedAccount; this.accountLinkingOptions = accountLinkingOptions; + this.disbursementDetails = disbursementDetails; + this.multiDisburseLoan = multiDisburseLoan; + this.fixedEmiAmount = fixedEmiAmount; + this.maxOutstandingLoanBalance = maxOutstandingLoanBalance; + + if(this.status != null && LoanStatus.fromInt(this.status.id().intValue()).isApproved()){ + this.canDisburse = true; + }else{ + boolean canDisburse = false; + if(this.multiDisburseLoan != null && this.multiDisburseLoan && this.disbursementDetails != null){ + for(DisbursementData disbursementData : this.disbursementDetails){ + if(!disbursementData.isDisbursed()){ + canDisburse = true; + } + } + } + this.canDisburse = canDisburse; + } + this.emiAmountVariations = emiAmountVariations; } public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData() { - return this.timeline.repaymentScheduleRelatedData(this.currency, this.principal, this.inArrearsTolerance, + return this.timeline.repaymentScheduleRelatedData(this.currency, this.principal,this.approvedPrincipal, this.inArrearsTolerance, this.feeChargesAtDisbursementCharged); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanApplicationTimelineData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanApplicationTimelineData.java index 15d922a38..07d235e9f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanApplicationTimelineData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanApplicationTimelineData.java @@ -122,9 +122,9 @@ public class LoanApplicationTimelineData { this.writeOffByLastname = writeOffByLastname; } - public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData(final CurrencyData currency, final BigDecimal principal, + public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData(final CurrencyData currency, final BigDecimal principal,final BigDecimal approvedPrincipal, final BigDecimal inArrearsTolerance, final BigDecimal totalFeeChargesAtDisbursement) { - return new RepaymentScheduleRelatedLoanData(this.expectedDisbursementDate, this.actualDisbursementDate, currency, principal, + return new RepaymentScheduleRelatedLoanData(this.expectedDisbursementDate, this.actualDisbursementDate, currency, principal,approvedPrincipal, inArrearsTolerance, totalFeeChargesAtDisbursement); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java new file mode 100644 index 000000000..7d9f20bbc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTermVariationsData.java @@ -0,0 +1,38 @@ +package org.mifosplatform.portfolio.loanaccount.data; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; + +public class LoanTermVariationsData { + + @SuppressWarnings("unused") + private final Long id; + private final EnumOptionData termType; + private final LocalDate termVariationApplicableFrom; + private final BigDecimal termValue; + + public LoanTermVariationsData(final Long id, final EnumOptionData termType, + final LocalDate termVariationApplicableFrom, final BigDecimal termValue) { + this.id = id; + this.termType = termType; + this.termVariationApplicableFrom = termVariationApplicableFrom; + this.termValue = termValue; + } + + + public EnumOptionData getTermType() { + return this.termType; + } + + + public LocalDate getTermApplicableFrom() { + return this.termVariationApplicableFrom; + } + + + public BigDecimal getTermValue() { + return this.termValue; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionData.java index 657815a5c..481b140d4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanTransactionData.java @@ -38,6 +38,7 @@ public class LoanTransactionData { private final BigDecimal overpaymentPortion; private final String externalId; private final AccountTransferData transfer; + private final BigDecimal fixedEmiAmount; // templates final Collection paymentTypeOptions; @@ -48,23 +49,24 @@ public class LoanTransactionData { loanTransactionData.type, loanTransactionData.paymentDetailData, loanTransactionData.currency, loanTransactionData.date, loanTransactionData.amount, loanTransactionData.principalPortion, loanTransactionData.interestPortion, loanTransactionData.feeChargesPortion, loanTransactionData.penaltyChargesPortion, loanTransactionData.overpaymentPortion, - paymentTypeOptions, loanTransactionData.externalId, loanTransactionData.transfer); + paymentTypeOptions, loanTransactionData.externalId, loanTransactionData.transfer,loanTransactionData.fixedEmiAmount); } public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, final String externalId, - final AccountTransferData transfer) { + final AccountTransferData transfer, BigDecimal fixedEmiAmount) { this(id, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, principalPortion, interestPortion, - feeChargesPortion, penaltyChargesPortion, overpaymentPortion, null, externalId, transfer); + feeChargesPortion, penaltyChargesPortion, overpaymentPortion, null, externalId, transfer,fixedEmiAmount); } public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - final Collection paymentTypeOptions, final String externalId, final AccountTransferData transfer) { + final Collection paymentTypeOptions, final String externalId, final AccountTransferData transfer, + final BigDecimal fixedEmiAmount) { this.id = id; this.officeId = officeId; this.officeName = officeName; @@ -81,6 +83,7 @@ public class LoanTransactionData { this.externalId = externalId; this.transfer = transfer; this.overpaymentPortion = overpaymentPortion; + this.fixedEmiAmount = fixedEmiAmount; } public LocalDate dateOf() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java index e1df92867..69cb3b5bf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java @@ -19,16 +19,18 @@ public class RepaymentScheduleRelatedLoanData { private final LocalDate actualDisbursementDate; private final CurrencyData currency; private final BigDecimal principal; + private final BigDecimal approvedPrincipal; private final BigDecimal inArrearsTolerance; private final BigDecimal totalFeeChargesAtDisbursement; public RepaymentScheduleRelatedLoanData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate, - final CurrencyData currency, final BigDecimal principal, final BigDecimal inArrearsTolerance, + final CurrencyData currency, final BigDecimal principal,final BigDecimal approvedPrincipal, final BigDecimal inArrearsTolerance, final BigDecimal totalFeeChargesAtDisbursement) { this.expectedDisbursementDate = expectedDisbursementDate; this.actualDisbursementDate = actualDisbursementDate; this.currency = currency; this.principal = principal; + this.approvedPrincipal = approvedPrincipal; this.inArrearsTolerance = inArrearsTolerance; this.totalFeeChargesAtDisbursement = totalFeeChargesAtDisbursement; } @@ -62,6 +64,6 @@ public class RepaymentScheduleRelatedLoanData { } public DisbursementData disbursementData() { - return new DisbursementData(this.expectedDisbursementDate, this.actualDisbursementDate, this.principal); + return new DisbursementData(null, this.expectedDisbursementDate, this.actualDisbursementDate, this.principal,this.approvedPrincipal); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 9c3acaa03..c54db3e5d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -46,6 +46,7 @@ import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper; import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.organisation.holiday.domain.Holiday; @@ -67,14 +68,20 @@ import org.mifosplatform.portfolio.collateral.data.CollateralData; import org.mifosplatform.portfolio.collateral.domain.LoanCollateral; import org.mifosplatform.portfolio.fund.domain.Fund; import org.mifosplatform.portfolio.group.domain.Group; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; +import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; +import org.mifosplatform.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; @@ -83,7 +90,6 @@ import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleM import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductBorrowerCycleVariations; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; @@ -92,6 +98,10 @@ import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.data.jpa.domain.AbstractPersistable; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + @Entity @Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"), @UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") }) @@ -275,42 +285,68 @@ public class Loan extends AbstractPersistable { private LoanLifecycleStateMachine loanLifecycleStateMachine; @Transient private LoanSummaryWrapper loanSummaryWrapper; + + @Column(name = "approved_principal", scale = 6, precision = 19, nullable = false) + private BigDecimal approvedPrincipal; + + @Column(name = "fixed_emi_amount", scale = 6, precision = 19, nullable = true) + private BigDecimal fixedEmiAmount; + + @Column(name = "max_outstanding_loan_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal maxOutstandingLoanBalance; + + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) + private Set disbursementDetails = new HashSet(); + + @OrderBy(value = "termApplicableFrom, id") + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) + private final Set loanTermVariations = new HashSet(); + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, - final Set collateral) { + final Set collateral, final BigDecimal fixedEmiAmount, final Set disbursementDetails, + final BigDecimal maxOutstandingLoanBalance) { final LoanStatus status = null; final Group group = null; final Boolean syncDisbursementWithMeeting = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount, + disbursementDetails,maxOutstandingLoanBalance); } public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, - final Boolean syncDisbursementWithMeeting) { + final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount, + final Set disbursementDetails,final BigDecimal maxOutstandingLoanBalance) { final LoanStatus status = null; final CodeValue loanPurpose = null; final Set collateral = null; final Client client = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount, + disbursementDetails,maxOutstandingLoanBalance); } public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, - final Boolean syncDisbursementWithMeeting) { + final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount, + final Set disbursementDetails,final BigDecimal maxOutstandingLoanBalance) { final LoanStatus status = null; final CodeValue loanPurpose = null; final Set collateral = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount, + disbursementDetails,maxOutstandingLoanBalance); } protected Loan() { @@ -320,7 +356,9 @@ public class Loan extends AbstractPersistable { private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, - final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting) { + final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting, + final BigDecimal fixedEmiAmount, final Set disbursementDetails, + final BigDecimal maxOutstandingLoanBalance) { this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); @@ -360,6 +398,10 @@ public class Loan extends AbstractPersistable { this.loanOfficerHistory = null; this.syncDisbursementWithMeeting = syncDisbursementWithMeeting; + this.fixedEmiAmount = fixedEmiAmount; + this.maxOutstandingLoanBalance = maxOutstandingLoanBalance; + this.disbursementDetails = disbursementDetails; + this.approvedPrincipal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount(); } private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) { @@ -1191,10 +1233,141 @@ public class Loan extends AbstractPersistable { actualChanges.put(loanTermFrequencyTypeParamName, newTermPeriodFrequencyType.getValue()); this.termPeriodFrequencyType = newValue; } + + final String principalParamName = "principal"; + if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.approvedPrincipal)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName); + this.approvedPrincipal = newValue; + } + + if (loanProduct.isMultiDisburseLoan()) { + updateDisbursementDetails(command, actualChanges); + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount)) { + this.fixedEmiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); + } + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName, this.maxOutstandingLoanBalance)) { + this.maxOutstandingLoanBalance = command.bigDecimalValueOfParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName); + } + if (this.disbursementDetails.isEmpty()) { + final String errorMessage = "For this loan product, disbursement details must be provided"; + throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + if(disbursementDetails.size() > loanProduct.maxTrancheCount()){ + final String errorMessage = "Number of tranche shouldn't be greter than "+loanProduct.maxTrancheCount(); + throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, + loanProduct.maxTrancheCount(),disbursementDetails.size()); + } + } else { + this.disbursementDetails.clear(); + this.fixedEmiAmount = null; + } return actualChanges; } + + private void updateDisbursementDetails(final JsonCommand command, final Map actualChanges) { + + List list = fetchDisbursementIds(); + + if (command.parameterExists(LoanApiConstants.disbursementDataParameterName)) { + final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); + if (disbursementDataArray != null && disbursementDataArray.size() > 0) { + String dateFormat = null; + Locale locale = null; + if (command.parsedJson().isJsonObject()) { + JsonObject topLevel = command.parsedJson().getAsJsonObject(); + final String dateFormatParameter = "dateFormat"; + + if (topLevel.has(dateFormatParameter) && topLevel.get(dateFormatParameter).isJsonPrimitive()) { + final JsonPrimitive primitive = topLevel.get(dateFormatParameter).getAsJsonPrimitive(); + dateFormat = primitive.getAsString(); + } + + final String localeParameter = "locale"; + if (topLevel.has(localeParameter) && topLevel.get(localeParameter).isJsonPrimitive()) { + final JsonPrimitive primitive = topLevel.get(localeParameter).getAsJsonPrimitive(); + String localeString = primitive.getAsString(); + locale = JsonParserHelper.localeFromString(localeString); + } + } + int i = 0; + do { + final JsonObject jsonObject = disbursementDataArray.get(i).getAsJsonObject(); + Date expectedDisbursementDate = null; + BigDecimal principal = null; + LocalDate date = null; + Long id = null; + if (jsonObject.has(LoanApiConstants.disbursementDateParameterName) + && jsonObject.get(LoanApiConstants.disbursementDateParameterName) != null + && jsonObject.get(LoanApiConstants.disbursementDateParameterName).isJsonPrimitive()) { + + final JsonPrimitive primitive = jsonObject.get(LoanApiConstants.disbursementDateParameterName).getAsJsonPrimitive(); + final String valueAsString = primitive.getAsString(); + if (StringUtils.isNotBlank(valueAsString)) { + date = JsonParserHelper.convertFrom(valueAsString, LoanApiConstants.disbursementDateParameterName, dateFormat, + locale); + } + } + if (date != null) { + expectedDisbursementDate = date.toDate(); + } + + if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName) + && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() + && StringUtils.isNotBlank((jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) { + principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal(); + } + if (jsonObject.has(LoanApiConstants.disbursementIdParameterName) + && jsonObject.get(LoanApiConstants.disbursementIdParameterName).isJsonPrimitive() + && StringUtils.isNotBlank((jsonObject.get(LoanApiConstants.disbursementIdParameterName).getAsString()))) { + id = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementIdParameterName).getAsLong(); + } + LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, null, principal); + if (id == null) { + disbursementDetails.updateLoan(this); + this.disbursementDetails.add(disbursementDetails); + actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal); + actualChanges.put("recalculateLoanSchedule", true); + } else { + list.remove(id); + LoanDisbursementDetails loanDisbursementDetail = fetchLoanDisbursementsById(id); + if (!loanDisbursementDetail.equals(disbursementDetails)) { + loanDisbursementDetail.copy(disbursementDetails); + actualChanges.put("disbursementDetailId", id); + actualChanges.put("recalculateLoanSchedule", true); + } + } + i++; + } while (i < disbursementDataArray.size()); + for (Long id : list) { + this.disbursementDetails.remove(fetchLoanDisbursementsById(id)); + actualChanges.put("recalculateLoanSchedule", true); + } + } + } + + } + + public LoanDisbursementDetails fetchLoanDisbursementsById(Long id) { + LoanDisbursementDetails loanDisbursementDetail = null; + for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) { + if (id.equals(disbursementDetail.getId())) { + loanDisbursementDetail = disbursementDetail; + break; + } + } + return loanDisbursementDetail; + } + + private List fetchDisbursementIds() { + List list = new ArrayList(); + for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) { + list.add(disbursementDetails.getId()); + } + return list; + } + private CollateralData[] listOfLoanCollateralData(final Set setOfLoanCollateral) { CollateralData[] existingLoanCollateral = null; @@ -1512,18 +1685,32 @@ public class Loan extends AbstractPersistable { return ids; } - public Map disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, + public void disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, final List existingReversedTransactionIds, final Map actualChanges, final PaymentDetail paymentDetail, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, final boolean allowTransactionsOnNonWorkingDay) { validateAccountStatus(LoanEvent.LOAN_DISBURSED); - updateLoanToPreDisbursalState(); + Date lastDusburseDate = this.actualDisbursementDate; final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus)); - if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { + boolean isMultiTrancheDisburse = false; + final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + if(LoanStatus.fromInt(this.loanStatus).isActive() && isAllTranchesNotDisbursed()){ + LoanDisbursementDetails details = fetchLastDisburseDetail(); + + if(details != null){ + lastDusburseDate = details.actualDisbursementDate(); + } + if(actualDisbursementDate.toDate().before(lastDusburseDate)){ + final String errorMsg = "Loan can't be disbursed before "+lastDusburseDate; + throw new LoanDisbursalException(errorMsg, "actualdisbursementdate.before.lastdusbursedate", lastDusburseDate,actualDisbursementDate.toDate()); + } + isMultiTrancheDisburse = true; + } + if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus)) || isMultiTrancheDisburse) { existingTransactionIds.addAll(findExistingTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); @@ -1531,10 +1718,43 @@ public class Loan extends AbstractPersistable { this.loanStatus = statusEnum.getValue(); actualChanges.put("status", LoanEnumerations.status(this.loanStatus)); - final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); - - this.actualDisbursementDate = actualDisbursementDate.toDate(); + Money disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal().zero(); + BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName); + if(this.actualDisbursementDate == null){ + this.actualDisbursementDate = actualDisbursementDate.toDate(); + } + BigDecimal diff = BigDecimal.ZERO; + Collection details = fetchUndisbursedDetail(); + if(principalDisbursed == null){ + disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal(); + if(!details.isEmpty()){ + disburseAmount = disburseAmount.zero(); + for(LoanDisbursementDetails disbursementDetails:details){ + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); + disburseAmount = disburseAmount.plus(disbursementDetails.principal()); + } + } + }else{ + disburseAmount = disburseAmount.plus(principalDisbursed); + + if(details.isEmpty()){ + diff = this.loanRepaymentScheduleDetail.getPrincipal().minus(principalDisbursed).getAmount(); + }else{ + for(LoanDisbursementDetails disbursementDetails:details){ + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate.toDate()); + diff = diff.add(disbursementDetails.principal().subtract(principalDisbursed)); + disbursementDetails.updatePrincipal(principalDisbursed); + } + } + this.loanRepaymentScheduleDetail.setPrincipal(this.loanRepaymentScheduleDetail.getPrincipal().minus(diff).getAmount()); + if(diff.compareTo(BigDecimal.ZERO) == -1){ + final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; + throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, + this.loanRepaymentScheduleDetail.getPrincipal().getAmount()); + } + } this.disbursedBy = currentUser; updateLoanScheduleDependentDerivedFields(); @@ -1546,9 +1766,15 @@ public class Loan extends AbstractPersistable { validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay); validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays); - handleDisbursementTransaction(paymentDetail, actualDisbursementDate, txnExternalId); - - if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { + handleDisbursementTransaction(paymentDetail, actualDisbursementDate, txnExternalId,disburseAmount); + BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); + boolean isEmiAmountChanged = false; + if(this.loanProduct.isMultiDisburseLoan() && emiAmount != null && emiAmount.compareTo(retriveLastEmiAmount()) != 0){ + LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(),actualDisbursementDate.toDate(),emiAmount,this); + this.loanTermVariations.add(loanVariationTerms); + isEmiAmountChanged = true; + } + if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || diff.compareTo(BigDecimal.ZERO) !=0 || isEmiAmountChanged) { regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); @@ -1561,9 +1787,114 @@ public class Loan extends AbstractPersistable { final LoanTransaction interestAppliedTransaction = LoanTransaction.applyInterest(getOffice(), this, interestApplied, actualDisbursementDate); this.loanTransactions.add(interestAppliedTransaction); + +// changedTransactionDetail = reprocessTransactionForDisbursement(changedTransactionDetail); } - return actualChanges; + } + + public ChangedTransactionDetail reprocessTransactionForDisbursement() { + ChangedTransactionDetail changedTransactionDetail = null; + for(LoanCharge loanCharge:this.charges){ + if (loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()) { + final Set chargePerInstallments = generateInstallmentLoanCharges(loanCharge); + loanCharge.addLoanInstallmentCharges(chargePerInstallments); + } + } + + if(this.loanProduct.isMultiDisburseLoan()){ + if(!this.actualDisbursementDate.equals(fetchLastDisburseDetail().getDisbursementDate())){ + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory + .determineProcessor(this.transactionProcessingStrategy); + final List allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), + allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges()); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(this); + } + + } + updateLoanSummaryDerivedFields(); + } + + + return changedTransactionDetail; + } + + private Collection fetchUndisbursedDetail(){ + Collection disbursementDetails = new ArrayList(); + Date date = null; + for(LoanDisbursementDetails disbursementDetail:this.disbursementDetails){ + if(disbursementDetail.actualDisbursementDate() == null){ + if(date == null || disbursementDetail.expectedDisbursementDate().equals(date)){ + disbursementDetails.add(disbursementDetail); + date =disbursementDetail.expectedDisbursementDate(); + }else if(disbursementDetail.expectedDisbursementDate().before(date)){ + disbursementDetails.clear(); + disbursementDetails.add(disbursementDetail); + date =disbursementDetail.expectedDisbursementDate(); + } + } + } + return disbursementDetails; + } + + private LoanDisbursementDetails fetchLastDisburseDetail(){ + LoanDisbursementDetails details = null; + Date date = this.actualDisbursementDate; + if(date != null){ + for(LoanDisbursementDetails disbursementDetail:this.disbursementDetails){ + if(disbursementDetail.actualDisbursementDate() != null){ + if(disbursementDetail.actualDisbursementDate().after(date) || + disbursementDetail.actualDisbursementDate().equals(date)){ + date =disbursementDetail.actualDisbursementDate(); + details = disbursementDetail; + } + } + } + } + return details; + } + + private BigDecimal getDisbursedAmount(){ + BigDecimal principal = BigDecimal.ZERO; + for(LoanDisbursementDetails disbursementDetail:this.disbursementDetails){ + if(disbursementDetail.actualDisbursementDate() != null){ + principal = principal.add(disbursementDetail.principal()); + } + } + return principal; + } + + private void removeDisbursementDetail(){ + Set details = new HashSet(this.disbursementDetails); + for(LoanDisbursementDetails disbursementDetail:details){ + if(disbursementDetail.actualDisbursementDate() == null){ + this.disbursementDetails.remove(disbursementDetail); + } + } + } + + private boolean isDisbursementAllowed(){ + boolean isAllowed = false; + for(LoanDisbursementDetails disbursementDetail:this.disbursementDetails){ + if(disbursementDetail.actualDisbursementDate() == null){ + isAllowed = true; + break; + } + } + return isAllowed; + } + + private boolean atleastOnceDisbursed(){ + boolean isDisbursed = false; + for(LoanDisbursementDetails disbursementDetail:this.disbursementDetails){ + if(disbursementDetail.actualDisbursementDate() != null){ + isDisbursed = true; + break; + } + } + return isDisbursed; } private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { @@ -1589,9 +1920,30 @@ public class Loan extends AbstractPersistable { final Integer loanTermFrequency = this.termFrequency; final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType); - final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, - loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), - calculatedRepaymentsStartingFromDate, getInArrearsTolerance(), this.loanRepaymentScheduleDetail); + final List disbursementData = new ArrayList(); + for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) { + disbursementData.add(disbursementDetails.toData()); + } + final List loanVariationTermsData = new ArrayList(); + boolean isDefaultEmiAmountReq = true; + for(LoanTermVariations variationTerms:this.loanTermVariations){ + if(variationTerms.getTermType().isEMIAmountVariation()){ + if(variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())){ + isDefaultEmiAmountReq =false; + } + loanVariationTermsData.add(variationTerms.toData()); + } + } + if(isDefaultEmiAmountReq){ + LoanTermVariationsData data = new LoanTermVariationsData(null, LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(), this.fixedEmiAmount); + loanVariationTermsData.add(data); + } + + final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms + .assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, getDisbursementDate(), + getExpectedFirstRepaymentOnDate(), calculatedRepaymentsStartingFromDate, getInArrearsTolerance(), + this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, + this.maxOutstandingLoanBalance,loanVariationTermsData); final LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms,charges(), isHolidayEnabled, holidays, workingDays); @@ -1600,10 +1952,10 @@ public class Loan extends AbstractPersistable { } private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn, - final String txnExternalId) { + final String txnExternalId, Money disburseAmt) { // track disbursement transaction final LoanTransaction disbursementTransaction = LoanTransaction.disbursement(getOffice(), - this.loanRepaymentScheduleDetail.getPrincipal(), paymentDetail, disbursedOn, txnExternalId); + disburseAmt, paymentDetail, disbursedOn, txnExternalId); disbursementTransaction.updateLoan(this); this.loanTransactions.add(disbursementTransaction); @@ -1620,27 +1972,29 @@ public class Loan extends AbstractPersistable { * "APPLY Charge" transactions are created for all other fees ( which * are created during disbursal but not repaid) **/ - Money disbursentMoney = Money.zero(getCurrency()); - final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getOffice(), disbursentMoney, null, disbursedOn, - null); - for (final LoanCharge charge : charges()) { - if (charge.isDueAtDisbursement()) { - if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - charge.markAsFullyPaid(); - // Add "Loan Charge Paid By" details to this transaction - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount()); - chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); - disbursentMoney = disbursentMoney.plus(charge.amount()); + if(disbursedOn.toDate().equals(this.actualDisbursementDate)){ + Money disbursentMoney = Money.zero(getCurrency()); + final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getOffice(), disbursentMoney, null, disbursedOn, + null); + for (final LoanCharge charge : charges()) { + if (charge.isDueAtDisbursement()) { + if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) { + charge.markAsFullyPaid(); + // Add "Loan Charge Paid By" details to this transaction + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount()); + chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); + disbursentMoney = disbursentMoney.plus(charge.amount()); + } + } else { + handleChargeAppliedTransaction(charge, disbursedOn); } - } else { - handleChargeAppliedTransaction(charge, disbursedOn); } - } - if (disbursentMoney.isGreaterThanZero()) { - final Money zero = Money.zero(getCurrency()); - chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); - chargesPayment.updateLoan(this); - this.loanTransactions.add(chargesPayment); + if (disbursentMoney.isGreaterThanZero()) { + final Money zero = Money.zero(getCurrency()); + chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); + chargesPayment.updateLoan(this); + this.loanTransactions.add(chargesPayment); + } } if (getApprovedOnDate() != null && disbursedOn.isBefore(getApprovedOnDate())) { @@ -1706,18 +2060,27 @@ public class Loan extends AbstractPersistable { actualChanges.put("status", LoanEnumerations.status(this.loanStatus)); final LocalDate actualDisbursementDate = getDisbursementDate(); - if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { + final boolean isScheduleRegenerateRequired = isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate); + this.actualDisbursementDate = null; + this.disbursedBy = null; + boolean isDisbueseAmtChanged = !this.approvedPrincipal.equals(this.loanRepaymentScheduleDetail.getPrincipal()); + this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal); + if(this.loanProduct.isMultiDisburseLoan()){ + for(final LoanDisbursementDetails details:this.disbursementDetails){ + details.updateActualDisbursementDate(null); + details.resetPrincipal(); + } + } + boolean isEmiAmountChanged = this.loanTermVariations.size()>0; + updateLoanToPreDisbursalState(); + if (isScheduleRegenerateRequired || isDisbueseAmtChanged || isEmiAmountChanged) { // clear off actual disbusrement date so schedule regeneration // uses expected date. - this.actualDisbursementDate = null; - this.disbursedBy = null; - + regenerateRepaymentSchedule(loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); } - this.actualDisbursementDate = null; - this.disbursedBy = null; actualChanges.put("actualDisbursementDate", ""); existingTransactionIds.addAll(findExistingTransactionIds()); @@ -1725,7 +2088,6 @@ public class Loan extends AbstractPersistable { reverseExistingTransactions(); - updateLoanToPreDisbursalState(); } return actualChanges; @@ -1749,6 +2111,7 @@ public class Loan extends AbstractPersistable { currentInstallment.resetDerivedComponents(); } + this.loanTermVariations.clear(); final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, charges()); @@ -1898,6 +2261,15 @@ public class Loan extends AbstractPersistable { loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount()); } } + + if(this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null){ + BigDecimal totalDisbursed = getDisbursedAmount(); + if(totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid())<0){ + final String errorMessage = "The transaction cannot be done before the loan disbursement: " + + getApprovedOnDate().toString(); + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage); + } + } final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategy); @@ -2142,7 +2514,6 @@ public class Loan extends AbstractPersistable { public ChangedTransactionDetail undoWrittenOff(final List existingTransactionIds, final List existingReversedTransactionIds){ validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO); - existingTransactionIds.addAll(findExistingTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); final LoanTransaction writeOffTransaction = findWriteOffTransaction(); @@ -2151,8 +2522,9 @@ public class Loan extends AbstractPersistable { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategy); final List allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges()); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction( + getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, + charges()); updateLoanSummaryDerivedFields(); return changedTransactionDetail; } @@ -2205,9 +2577,17 @@ public class Loan extends AbstractPersistable { return this.loanRepaymentScheduleDetail.getCurrency(); } - public LoanTransaction closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, + public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, - final AppUser currentUser) { + final AppUser currentUser,final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency, + final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, + final WorkingDays workingDays) { + + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory + .determineProcessor(this.transactionProcessingStrategy); + ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, + workingDays, loanRepaymentScheduleTransactionProcessor); validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING); @@ -2244,7 +2624,7 @@ public class Loan extends AbstractPersistable { final String errorMessage = "The date on which a loan is written off cannot be in the future."; throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate); } - + loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, txnExternalId); final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, this.loanTransactions); if (!isLastTransaction) { @@ -2255,18 +2635,58 @@ public class Loan extends AbstractPersistable { this.loanTransactions.add(loanTransaction); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory - .determineProcessor(this.transactionProcessingStrategy); loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), this.repaymentScheduleInstallments); updateLoanSummaryDerivedFields(); } - - return loanTransaction; + if(changedTransactionDetail == null){ + changedTransactionDetail = new ChangedTransactionDetail(); + } + changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); + return changedTransactionDetail; } - public LoanTransaction close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds) { + private ChangedTransactionDetail closeDisbursements(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency, + final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, + final WorkingDays workingDays, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + ChangedTransactionDetail changedTransactionDetail = null; + if(isDisbursementAllowed() && atleastOnceDisbursed()){ + this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount()); + removeDisbursementDetail(); + regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, + holidays, workingDays); + + final List allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), + allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges()); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(this); + this.loanTransactions.add(mapEntry.getValue()); + } + updateLoanSummaryDerivedFields(); + LoanTransaction loanTransaction = findlatestTransaction(); + doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + } + return changedTransactionDetail; + } + + private LoanTransaction findlatestTransaction(){ + LoanTransaction transaction = null; + for(LoanTransaction loanTransaction:this.loanTransactions){ + if(!loanTransaction.isReversed() && + (transaction == null || transaction.getTransactionDate().isBefore(loanTransaction.getTransactionDate()))){ + transaction = loanTransaction; + } + } + return transaction; + } + + public ChangedTransactionDetail close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, + final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, + final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency, + final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, + final WorkingDays workingDays) { + validateAccountStatus(LoanEvent.LOAN_CLOSED); @@ -2291,6 +2711,10 @@ public class Loan extends AbstractPersistable { final String errorMessage = "The date on which a loan is closed cannot be in the future."; throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate); } + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory + .determineProcessor(this.transactionProcessingStrategy); + ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, + workingDays, loanRepaymentScheduleTransactionProcessor); LoanTransaction loanTransaction = null; if (isOpen()) { @@ -2304,7 +2728,6 @@ public class Loan extends AbstractPersistable { changes.put("status", LoanEnumerations.status(this.loanStatus)); } this.closedOnDate = closureDate.toDate(); - loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, txnExternalId); final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, this.loanTransactions); if (!isLastTransaction) { @@ -2315,8 +2738,6 @@ public class Loan extends AbstractPersistable { this.loanTransactions.add(loanTransaction); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory - .determineProcessor(this.transactionProcessingStrategy); loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), this.repaymentScheduleInstallments); @@ -2346,8 +2767,12 @@ public class Loan extends AbstractPersistable { throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString()); } } - - return loanTransaction; + + if(changedTransactionDetail == null){ + changedTransactionDetail = new ChangedTransactionDetail(); + } + changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); + return changedTransactionDetail; } /** @@ -2440,6 +2865,13 @@ public class Loan extends AbstractPersistable { private boolean isOpen() { return status().isActive(); } + + private boolean isAllTranchesNotDisbursed(){ + return this.loanProduct.isMultiDisburseLoan() + && (LoanStatus.fromInt(this.loanStatus).isActive() + ||LoanStatus.fromInt(this.loanStatus).isApproved()) + && isDisbursementAllowed(); + } private boolean hasDisbursementTransaction() { boolean hasRepaymentTransaction = false; @@ -2483,6 +2915,34 @@ public class Loan extends AbstractPersistable { } return disbursementDate; } + + public LocalDate getExpectedDisbursedOnLocalDateForTemplate(){ + + LocalDate expectedDisbursementDate = null; + if (this.expectedDisbursementDate != null) { + expectedDisbursementDate = new LocalDate(this.expectedDisbursementDate); + } + + Collection details = fetchUndisbursedDetail(); + if(!details.isEmpty()){ + for(LoanDisbursementDetails disbursementDetails:details){ + expectedDisbursementDate = new LocalDate(disbursementDetails.expectedDisbursementDate()); + } + } + return expectedDisbursementDate; + } + + public BigDecimal getDisburseAmountForTemplate(){ + BigDecimal principal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount(); + Collection details = fetchUndisbursedDetail(); + if(!details.isEmpty()){ + principal =BigDecimal.ZERO; + for(LoanDisbursementDetails disbursementDetails:details){ + principal = principal.add(disbursementDetails.principal()); + } + } + return principal; + } public LocalDate getExpectedFirstRepaymentOnDate() { LocalDate firstRepaymentDate = null; @@ -2498,7 +2958,14 @@ public class Loan extends AbstractPersistable { } private boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) { - return !new LocalDate(this.expectedDisbursementDate).isEqual(actualDisbursedOnDate); + boolean isRegenerationRequired = false; + if(this.loanProduct.isMultiDisburseLoan()){ + LoanDisbursementDetails details = fetchLastDisburseDetail(); + if(!details.expectedDisbursementDate().equals(details.actualDisbursementDate())){ + isRegenerationRequired = true; + } + } + return !new LocalDate(this.expectedDisbursementDate).isEqual(actualDisbursedOnDate) || isRegenerationRequired; } private boolean isRepaymentScheduleRegenerationRequiredForDisbursement(final LocalDate actualDisbursementDate) { @@ -2947,6 +3414,9 @@ public class Loan extends AbstractPersistable { } public Integer getLoanProductLoanCounter() { + if(this.loanProductCounter == null){ + return 0; + } return this.loanProductCounter; } @@ -3115,7 +3585,8 @@ public class Loan extends AbstractPersistable { } break; case LOAN_DISBURSED: - if (!(isApproved() && isNotDisbursed())) { + if ((!(isApproved() && isNotDisbursed()) && !this.loanProduct.isMultiDisburseLoan()) || + (this.loanProduct.isMultiDisburseLoan() && !isAllTranchesNotDisbursed())) { final String defaultUserMessage = "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state."; final ApiParameterError error = ApiParameterError.generalError( "error.msg.loan.disbursal.account.is.not.approve.not.disbursed.state", defaultUserMessage); @@ -3188,6 +3659,14 @@ public class Loan extends AbstractPersistable { dataValidationErrors.add(error); } break; + case LOAN_EDIT_MULTI_DISBURSE_DATE: + if (isClosed()) { + final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError( + "error.msg.loan.edit.disbursement.account.is.not.active", defaultUserMessage); + dataValidationErrors.add(error); + } + break; default: break; } @@ -3214,5 +3693,51 @@ public class Loan extends AbstractPersistable { } return list; } + + public Set getDisbursementDetails() { + return this.disbursementDetails; + } + + public ChangedTransactionDetail updateDisbursementDateForTranche(final LoanDisbursementDetails disbursementDetails,final JsonCommand command,final List existingTransactionIds, + final List existingReversedTransactionIds, final Map actualChanges, + final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency, + final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, + final WorkingDays workingDays){ + validateAccountStatus(LoanEvent.LOAN_EDIT_MULTI_DISBURSE_DATE); + final LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.disbursementDateParameterName); + disbursementDetails.updateExpectedDisbursementDate(expectedDisbursementDate.toDate()); + actualChanges.put(LoanApiConstants.disbursementDateParameterName, command.stringValueOfParameterNamed(LoanApiConstants.disbursementDateParameterName)); + actualChanges.put(LoanApiConstants.disbursementIdParameterName, command.stringValueOfParameterNamed(LoanApiConstants.disbursementIdParameterName)); + existingTransactionIds.addAll(findExistingTransactionIds()); + existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, + holidays, workingDays); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory + .determineProcessor(this.transactionProcessingStrategy); + final List allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), + allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges()); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(this); + this.loanTransactions.add(mapEntry.getValue()); + } + + return changedTransactionDetail; + } + + + public BigDecimal retriveLastEmiAmount(){ + BigDecimal emiAmount = this.fixedEmiAmount; + Date startDate = this.getDisbursementDate().toDate(); + for(LoanTermVariations loanTermVariations:this.loanTermVariations){ + if(loanTermVariations.getTermType().isEMIAmountVariation() && + !startDate.after(loanTermVariations.getTermApplicableFrom())){ + startDate = loanTermVariations.getTermApplicableFrom(); + emiAmount = loanTermVariations.getTermValue(); + } + } + return emiAmount; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index 4b847b9e3..37d85bc61 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -662,15 +662,14 @@ public class LoanCharge extends AbstractPersistable { amountPaidToDate = amountPaidToDate.plus(amountOutstanding); this.amountPaid = amountPaidToDate.getAmount(); this.amountOutstanding = BigDecimal.ZERO; + this.paid = true; + } else { amountPaidOnThisCharge = processAmount; amountPaidToDate = amountPaidToDate.plus(processAmount); this.amountPaid = amountPaidToDate.getAmount(); this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency()); } - - this.paid = determineIfFullyPaid(); - return amountPaidOnThisCharge; } @@ -767,6 +766,10 @@ public class LoanCharge extends AbstractPersistable { public boolean hasNoLoanInstallmentCharges(){ return this.loanInstallmentCharge.isEmpty(); } + + public Set installmentCharges(){ + return this.loanInstallmentCharge; + } public LoanRepaymentScheduleInstallment fetchRepaymentInstallment(final Money trasferedAmount){ for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanDisbursementDetails.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanDisbursementDetails.java new file mode 100644 index 000000000..dc5b3324f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanDisbursementDetails.java @@ -0,0 +1,129 @@ +/** + * 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.domain; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.LocalDate; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_disbursement_detail") +public class LoanDisbursementDetails extends AbstractPersistable { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "loan_id", nullable = false) + private Loan loan; + + @Temporal(TemporalType.DATE) + @Column(name = "expected_disburse_date") + private Date expectedDisbursementDate; + + @Temporal(TemporalType.DATE) + @Column(name = "disbursedon_date") + private Date actualDisbursementDate; + + @Column(name = "principal", scale = 6, precision = 19, nullable = false) + private BigDecimal principal; + + @Column(name = "approved_principal", scale = 6, precision = 19, nullable = false) + private BigDecimal approvedPrincipal; + + protected LoanDisbursementDetails() { + + } + + public LoanDisbursementDetails(final Date expectedDisbursementDate, final Date actualDisbursementDate, final BigDecimal principal) { + this.expectedDisbursementDate = expectedDisbursementDate; + this.actualDisbursementDate = actualDisbursementDate; + this.principal = principal; + this.approvedPrincipal = principal; + } + + public void updateLoan(final Loan loan) { + this.loan = loan; + } + + @Override + public boolean equals(final Object obj) { + final LoanDisbursementDetails loanDisbursementDetails = (LoanDisbursementDetails) obj; + if (loanDisbursementDetails.principal.equals(this.principal) + && loanDisbursementDetails.expectedDisbursementDate.equals(this.expectedDisbursementDate) + && loanDisbursementDetails.approvedPrincipal.equals(this.approvedPrincipal)) { return true; } + return false; + } + + public void copy(final LoanDisbursementDetails disbursementDetails) { + this.principal = disbursementDetails.principal; + this.approvedPrincipal = disbursementDetails.approvedPrincipal; + this.expectedDisbursementDate = disbursementDetails.expectedDisbursementDate; + this.actualDisbursementDate = disbursementDetails.actualDisbursementDate; + } + + public Date expectedDisbursementDate() { + return this.expectedDisbursementDate; + } + + public Date actualDisbursementDate() { + return this.actualDisbursementDate; + } + + public BigDecimal principal() { + return this.principal; + } + + public void updatePrincipal(BigDecimal principal){ + this.principal = principal; + } + + + public Date getDisbursementDate() { + Date disbursementDate = this.expectedDisbursementDate; + if (this.actualDisbursementDate != null) { + disbursementDate = this.actualDisbursementDate; + } + return disbursementDate; + } + + public DisbursementData toData() { + LocalDate expectedDisburseDate = null; + LocalDate actualDisburseDate = null; + if(this.expectedDisbursementDate != null){ + expectedDisburseDate = new LocalDate(this.expectedDisbursementDate); + actualDisburseDate = expectedDisburseDate; + } + if(getDisbursementDate() != null){ + actualDisburseDate = new LocalDate(getDisbursementDate()); + } + return new DisbursementData(getId(), expectedDisburseDate, actualDisburseDate, this.principal,this.approvedPrincipal); + } + + + public void updateActualDisbursementDate(Date actualDisbursementDate) { + this.actualDisbursementDate = actualDisbursementDate; + } + + public BigDecimal resetPrincipal() { + return this.principal = this.approvedPrincipal; + } + + + public void updateExpectedDisbursementDate(Date expectedDisbursementDate) { + this.expectedDisbursementDate = expectedDisbursementDate; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java index dc25aea65..185acf836 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanEvent.java @@ -24,5 +24,6 @@ public enum LoanEvent { LOAN_RESCHEDULE, // INTERST_REBATE_OWED, // LOAN_OVERPAYMENT, LOAN_CHARGE_PAYMENT, - LOAN_CLOSED; + LOAN_CLOSED, + LOAN_EDIT_MULTI_DISBURSE_DATE; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 92e6fac95..10ff41c21 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -104,7 +104,9 @@ public class LoanRepaymentScheduleProcessingWrapper { if(loanCharge.isFeeCharge()){ if(loanCharge.isInstalmentFee()){ LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + if(loanChargePerInstallment != null){ + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + } }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); } @@ -123,7 +125,9 @@ public class LoanRepaymentScheduleProcessingWrapper { if(loanCharge.isFeeCharge()){ if(loanCharge.isInstalmentFee()){ LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + if(loanChargePerInstallment != null){ + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + } }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency)); } @@ -186,7 +190,9 @@ public class LoanRepaymentScheduleProcessingWrapper { if (loanCharge.isPenaltyCharge()) { if(loanCharge.isInstalmentFee()){ LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + if(loanChargePerInstallment != null){ + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + } }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); } @@ -205,7 +211,9 @@ public class LoanRepaymentScheduleProcessingWrapper { if (loanCharge.isPenaltyCharge()) { if(loanCharge.isInstalmentFee()){ LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + if(loanChargePerInstallment!= null){ + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + } }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency)); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java index 81e7af512..9c2c7fe43 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java @@ -279,4 +279,9 @@ public final class LoanSummary { public BigDecimal getTotalPrincipalDisbursed() { return this.totalPrincipalDisbursed; } + + + public BigDecimal getTotalPrincipalRepaid() { + return this.totalPrincipalRepaid; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java new file mode 100644 index 000000000..2442e3dff --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -0,0 +1,38 @@ +package org.mifosplatform.portfolio.loanaccount.domain; + +public enum LoanTermVariationType { + + INVALID(0, "loanTermType.invalid"), // + EMI_AMOUNT(1, "loanTermType.emiAmount"); // + + private final Integer value; + private final String code; + + private LoanTermVariationType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static LoanTermVariationType fromInt(final Integer value) { + + LoanTermVariationType enumeration = LoanTermVariationType.INVALID; + switch (value) { + case 1: + enumeration = LoanTermVariationType.EMI_AMOUNT; + break; + } + return enumeration; + } + + public boolean isEMIAmountVariation() { + return this.value.equals(LoanTermVariationType.EMI_AMOUNT.getValue()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java new file mode 100644 index 000000000..3701ea1f5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTermVariations.java @@ -0,0 +1,74 @@ +package org.mifosplatform.portfolio.loanaccount.domain; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; +import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_term_variations") +public class LoanTermVariations extends AbstractPersistable { + + @SuppressWarnings("unused") + @ManyToOne(optional = false) + @JoinColumn(name = "loan_id", nullable = false) + private Loan loan; + + @Column(name = "term_type", nullable = false) + private Integer termType; + + @Temporal(TemporalType.DATE) + @Column(name = "applicable_from") + private Date termApplicableFrom; + + @Column(name = "term_value", scale = 6, precision = 19) + private BigDecimal termValue; + + + public LoanTermVariations(final Integer termType,final Date termApplicableFrom,final BigDecimal termValue,final Loan loan){ + + this.loan = loan; + this.termApplicableFrom = termApplicableFrom; + this.termType = termType; + this.termValue = termValue; + } + + protected LoanTermVariations(){ + + } + + + public LoanTermVariationType getTermType() { + return LoanTermVariationType.fromInt(this.termType); + } + + public LoanTermVariationsData toData() { + LocalDate termStartDate = new LocalDate(this.termApplicableFrom); + EnumOptionData type = LoanEnumerations.loanvariationType(this.termType); + return new LoanTermVariationsData(getId(), type, termStartDate, this.termValue); + } + + + public Date getTermApplicableFrom() { + return this.termApplicableFrom; + } + + + public BigDecimal getTermValue() { + return this.termValue; + } + + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index ef4e44356..0d91e5232 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -441,7 +441,7 @@ public final class LoanTransaction extends AbstractPersistable { } return new LoanTransactionData(getId(), this.office.getId(), this.office.getName(), transactionType, paymentDetailData, currencyData, getTransactionDate(), this.amount, this.principalPortion, this.interestPortion, this.feeChargesPortion, - this.penaltyChargesPortion, this.overPaymentPortion, this.externalId, transfer); + this.penaltyChargesPortion, this.overPaymentPortion, this.externalId, transfer, null); } public Map toMapData(final CurrencyData currencyData) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/ExceedingTrancheCountException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/ExceedingTrancheCountException.java new file mode 100644 index 000000000..20ca80d3d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/ExceedingTrancheCountException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.loanaccount.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ExceedingTrancheCountException extends AbstractPlatformDomainRuleException { + + public ExceedingTrancheCountException(final String entity, final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg." + entity + ".exceeding.max.tranche.count", defaultUserMessage, defaultUserMessageArgs); + } + +} 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 4f7d6fd25..d476dc52c 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,4 +9,7 @@ public class LoanDisbursalException extends AbstractPlatformDomainRuleException + "` are not allowed to co-exist", new Object[] { cureentProduct, restrictedProduct }); } + public LoanDisbursalException(final String defaultUserMessage,final String entity,final Object... defaultUserMessageArgs) { + super("error.msg.loan." + entity , defaultUserMessage, defaultUserMessageArgs); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MultiDisbursementDataRequiredException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MultiDisbursementDataRequiredException.java new file mode 100644 index 000000000..5d37381c0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/exception/MultiDisbursementDataRequiredException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.loanaccount.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class MultiDisbursementDataRequiredException extends AbstractPlatformDomainRuleException { + + public MultiDisbursementDataRequiredException(final String entity, final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg." + entity + ".required", defaultUserMessage, defaultUserMessageArgs); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UpdateLoanDisbuseDateCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UpdateLoanDisbuseDateCommandHandler.java new file mode 100644 index 000000000..767de6383 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/handler/UpdateLoanDisbuseDateCommandHandler.java @@ -0,0 +1,32 @@ +/** + * 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.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UpdateLoanDisbuseDateCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Autowired + public UpdateLoanDisbuseDateCommandHandler(final LoanWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.updateDisbursementDateForTranche(command.getLoanId(), command.entityId(), command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index 27ee6fb82..259099a86 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -72,7 +72,7 @@ public class LoanSchedulePeriodData { public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed, final BigDecimal feeChargesDueAtTimeOfDisbursement, final boolean isDisbursed) { - final Integer periodNumber = Integer.valueOf(0); + final Integer periodNumber = null; final LocalDate from = null; return new LoanSchedulePeriodData(periodNumber, from, disbursementDate, principalDisbursed, feeChargesDueAtTimeOfDisbursement, isDisbursed); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 15c3be22e..181fee98e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -19,7 +19,11 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.organisation.workingdays.domain.WorkingDays; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementDisbursementDateException; +import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException; +import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; /** * @@ -35,10 +39,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final List holidays, final WorkingDays workingDays) { // 1. generate list of proposed schedule due dates - final List scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms, isHolidayEnabled, holidays, + final LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, isHolidayEnabled, holidays, workingDays); - - final LocalDate loanEndDate = scheduledDates.get(scheduledDates.size() - 1); loanApplicationTerms.updateLoanEndDate(loanEndDate); // 2. determine the total charges due at time of disbursement @@ -46,7 +48,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // 3. setup variables for tracking important facts required for loan // schedule generation. - final Money principalDisbursed = loanApplicationTerms.getPrincipal(); + Money principalDisbursed = loanApplicationTerms.getPrincipal(); + final Money expectedPrincipalDisburse = loanApplicationTerms.getPrincipal(); final MonetaryCurrency currency = principalDisbursed.getCurrency(); final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); @@ -67,20 +70,44 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( this.paymentPeriodsInOneYearCalculator, mc); - // 5. loop over the known set of 'scheduled due dates' - final LocalDate idealDisbursementDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( - loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), scheduledDates); - LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate(); - LocalDate periodStartDateApplicableForInterest = periodStartDate; + LocalDate actualRepaymentDate = periodStartDate; + LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(periodStartDate, loanApplicationTerms); + final LocalDate idealDisbursementDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( + loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), firstRepaymentdate); + + + LocalDate periodStartDateApplicableForInterest = periodStartDate; + int periodNumber = 1; Money totalCumulativePrincipal = principalDisbursed.zero(); Money totalCumulativeInterest = principalDisbursed.zero(); Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero(); Money outstandingBalance = principalDisbursed; - for (final LocalDate scheduledDueDate : scheduledDates) { - + if(loanApplicationTerms.isMultiDisburseLoan()){ + BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, periodStartDate, periods,chargesDueAtTimeOfDisbursement); + principalDisbursed = principalDisbursed.zero().plus(disburseAmt); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(disburseAmt)); + outstandingBalance = outstandingBalance.zero().plus(disburseAmt); + } + while(!outstandingBalance.isZero()) { + actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms); + LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, + isHolidayEnabled, holidays, workingDays); final int daysInPeriod = Days.daysBetween(periodStartDate, scheduledDueDate).getDays(); + if(loanApplicationTerms.isMultiDisburseLoan()){ + loanApplicationTerms.setFixedEmiAmountForPeriod(scheduledDueDate); + BigDecimal disburseAmt = disbursementForPeriod(loanApplicationTerms, periodStartDate, scheduledDueDate, periods,BigDecimal.ZERO); + principalDisbursed = principalDisbursed.plus(disburseAmt); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseAmt)); + outstandingBalance = outstandingBalance.plus(disburseAmt); + if(loanApplicationTerms.getMaxOutstandingBalance() != null && + outstandingBalance.isGreaterThan(loanApplicationTerms.getMaxOutstandingBalance())){ + String errorMsg = "Outstanding balance must not exceed the amount: "+loanApplicationTerms.getMaxOutstandingBalance(); + throw new MultiDisbursementOutstandingAmoutException(errorMsg, + loanApplicationTerms.getMaxOutstandingBalance().getAmount(),disburseAmt); + } + } int daysInPeriodApplicableForInterest = daysInPeriod; if (periodStartDate.isBefore(idealDisbursementDate)) { @@ -99,6 +126,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, daysInPeriodApplicableForInterest, outstandingBalance, loanApplicationTerms, periodNumber, mc); + if(loanApplicationTerms.getFixedEmiAmount() !=null + && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1){ + String errorMsg = "EMI amount must be greter than : "+principalInterestForThisPeriod.interest().getAmount(); + throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), + loanApplicationTerms.getFixedEmiAmount()); + } // update cumulative fields for principal & interest totalCumulativePrincipal = totalCumulativePrincipal.plus(principalInterestForThisPeriod.principal()); totalCumulativeInterest = totalCumulativeInterest.plus(principalInterestForThisPeriod.interest()); @@ -107,12 +140,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // 6. update outstandingLoanBlance using correct 'principalDue' outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal()); + Money feeChargesForInstallment = principalDisbursed.zero(); + Money penaltyChargesForInstallment = principalDisbursed.zero(); + if(!(loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getFixedEmiAmount()!=null)){ // 7. determine fees and penalties - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, currency, - principalInterestForThisPeriod, principalDisbursed, totalInterestChargedForFullLoanTerm, numberOfRepayments); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, - currency, principalInterestForThisPeriod, principalDisbursed, totalInterestChargedForFullLoanTerm, numberOfRepayments); - + feeChargesForInstallment = cumulativeFeeChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, currency, + principalInterestForThisPeriod, principalDisbursed, totalInterestChargedForFullLoanTerm, numberOfRepayments); + penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(periodStartDate, scheduledDueDate, loanCharges, + currency, principalInterestForThisPeriod, principalDisbursed, totalInterestChargedForFullLoanTerm, numberOfRepayments); + } // 8. sum up real totalInstallmentDue from components final Money totalInstallmentDue = principalInterestForThisPeriod.principal() // .plus(principalInterestForThisPeriod.interest()) // @@ -137,6 +173,31 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener periodNumber++; } + + if(principalDisbursed.isNotEqualTo(expectedPrincipalDisburse)){ + final String errorMsg = "One of the Disbursement date is not falling on Loan Schedule"; + throw new MultiDisbursementDisbursementDateException(errorMsg); + } + + if(loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getFixedEmiAmount()!=null + && !loanCharges.isEmpty()){ + // 7. determine fees and penalties + for(LoanScheduleModelPeriod loanScheduleModelPeriod:periods){ + if(loanScheduleModelPeriod.isRepaymentPeriod()){ + PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()), + Money.of(currency, loanScheduleModelPeriod.interestDue()), null); + Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), + loanScheduleModelPeriod.periodDueDate(), loanCharges, currency,principalInterest, principalDisbursed, + totalCumulativeInterest, numberOfRepayments); + Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), + loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed, totalCumulativeInterest, + numberOfRepayments); + totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); + totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); + loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount()); + } + } + } return LoanScheduleModel.from(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected, totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, @@ -161,15 +222,49 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } return chargesDueAtTimeOfDisbursement; } + + private BigDecimal disbursementForPeriod(final LoanApplicationTerms loanApplicationTerms,LocalDate startDate,LocalDate endDate, + final Collection periods,final BigDecimal chargesDueAtTimeOfDisbursement){ + BigDecimal principal = BigDecimal.ZERO; + MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency(); + for(DisbursementData disbursementData:loanApplicationTerms.getDisbursementDatas()){ + if(disbursementData.isDueForDisbursement(startDate, endDate)){ + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + disbursementData.disbursementDate(),Money.of(currency,disbursementData.amount()),chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + principal = principal.add(disbursementData.amount()); + } + } + return principal; + } + + private BigDecimal getDisbursementAmount(final LoanApplicationTerms loanApplicationTerms,LocalDate disbursementDate, + final Collection periods,final BigDecimal chargesDueAtTimeOfDisbursement){ + BigDecimal principal = BigDecimal.ZERO; + MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency(); + for(DisbursementData disbursementData:loanApplicationTerms.getDisbursementDatas()){ + if(disbursementData.disbursementDate().equals(disbursementDate)){ + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + disbursementData.disbursementDate(),Money.of(currency,disbursementData.amount()),chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + principal = principal.add(disbursementData.amount()); + } + } + return principal; + } private Collection createNewLoanScheduleListWithDisbursementDetails(final int numberOfRepayments, final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) { - final Collection periods = new ArrayList(numberOfRepayments + 1); - - final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( - loanApplicationTerms, chargesDueAtTimeOfDisbursement); - periods.add(disbursementPeriod); + Collection periods = null ; + if(loanApplicationTerms.isMultiDisburseLoan()){ + periods = new ArrayList(numberOfRepayments + loanApplicationTerms.getDisbursementDatas().size()); + }else{ + periods = new ArrayList(numberOfRepayments + 1); + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + loanApplicationTerms, chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + } return periods; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 80e183142..aa233e515 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -5,7 +5,6 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; -import java.util.ArrayList; import java.util.List; import org.joda.time.LocalDate; @@ -18,41 +17,47 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override - public List generate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, + public LocalDate getLastRepaymentDate(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays) { final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); - final List dueRepaymentPeriodDates = new ArrayList(numberOfRepayments); - - LocalDate startDate = loanApplicationTerms.getExpectedDisbursementDate(); - final LocalDate firstRepaymentPeriodDate = loanApplicationTerms.getCalculatedRepaymentsStartingFromLocalDate(); + LocalDate lastRepaymentDate = loanApplicationTerms.getExpectedDisbursementDate(); for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { - - LocalDate dueRepaymentPeriodDate = startDate; - if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { - dueRepaymentPeriodDate = firstRepaymentPeriodDate; - } else { - dueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, startDate); - } - - startDate = dueRepaymentPeriodDate; - final LocalDate nextDueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, dueRepaymentPeriodDate); - - dueRepaymentPeriodDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(dueRepaymentPeriodDate, nextDueRepaymentPeriodDate, - workingDays); - - if (isHolidayEnabled) { - dueRepaymentPeriodDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(dueRepaymentPeriodDate, holidays); - } - - dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); - } - - return dueRepaymentPeriodDates; + lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms); + } + lastRepaymentDate = adjustRepaymentDate(lastRepaymentDate, loanApplicationTerms, isHolidayEnabled, holidays, workingDays); + return lastRepaymentDate; } + + @Override + public LocalDate generateNextRepaymentDate(final LocalDate lastRepaymentDate,final LoanApplicationTerms loanApplicationTerms){ + final LocalDate firstRepaymentPeriodDate = loanApplicationTerms.getCalculatedRepaymentsStartingFromLocalDate(); + LocalDate dueRepaymentPeriodDate = null; + if(firstRepaymentPeriodDate !=null && firstRepaymentPeriodDate.isAfter(lastRepaymentDate)){ + dueRepaymentPeriodDate = firstRepaymentPeriodDate; + }else{ + dueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, lastRepaymentDate); + } + return dueRepaymentPeriodDate; + } + + @Override + public LocalDate adjustRepaymentDate(final LocalDate dueRepaymentPeriodDate,final LoanApplicationTerms loanApplicationTerms,final boolean isHolidayEnabled, + final List holidays, final WorkingDays workingDays){ + LocalDate adjustedDate = dueRepaymentPeriodDate; + final LocalDate nextDueRepaymentPeriodDate = getRepaymentPeriodDate(loanApplicationTerms, adjustedDate); + adjustedDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(adjustedDate, nextDueRepaymentPeriodDate, + workingDays); + if (isHolidayEnabled) { + adjustedDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(adjustedDate, holidays); + } + return adjustedDate; + } + + private LocalDate getRepaymentPeriodDate(final LoanApplicationTerms loanApplicationTerms, final LocalDate startDate) { final int repaidEvery = loanApplicationTerms.getRepaymentEvery(); LocalDate dueRepaymentPeriodDate = startDate; @@ -77,9 +82,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(final PeriodFrequencyType repaymentPeriodFrequencyType, - final int repaidEvery, final List scheduledDates) { - - final LocalDate firstRepaymentDate = scheduledDates.get(0); + final int repaidEvery, final LocalDate firstRepaymentDate) { LocalDate idealDisbursementDate = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java index 454d54129..9c78be4a4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java @@ -38,4 +38,16 @@ public class FinanicalFunctions { } return payment; } + + public static int nop(final double interestRateFraction, final double emiAmount, final double principal, + final double futureValue, final boolean type){ + double numberOfPayments = 0; + if (interestRateFraction == 0) { + numberOfPayments = ((Double)(-1 * (futureValue + principal) / emiAmount)).intValue(); + }else{ + final double r1 = interestRateFraction + 1; + numberOfPayments = (futureValue + principal * Math.pow(r1, emiAmount)) * interestRateFraction + / ((type ? r1 : 1) * (1 - Math.pow(r1, emiAmount))); } + return Double.valueOf(numberOfPayments).intValue(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index e954102a0..49e2063ef 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -7,13 +7,17 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.math.BigDecimal; import java.math.MathContext; +import java.math.RoundingMode; import java.util.Date; +import java.util.List; import org.joda.time.Days; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; @@ -36,7 +40,7 @@ public final class LoanApplicationTerms { private final BigDecimal annualNominalInterestRate; private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; - private final Money principal; + private Money principal; private final LocalDate expectedDisbursementDate; private final LocalDate repaymentsStartingFromDate; private final LocalDate calculatedRepaymentsStartingFromDate; @@ -85,6 +89,16 @@ public final class LoanApplicationTerms { // added private LocalDate loanEndDate; + + private List disbursementDatas; + + private boolean multiDisburseLoan; + + private BigDecimal fixedEmiAmount; + + private BigDecimal maxOutstandingBalance; + + private List emiAmountVariations; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -95,19 +109,24 @@ public final class LoanApplicationTerms { final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, - final Money inArrearsTolerance) { + final Money inArrearsTolerance,final boolean multiDisburseLoan,final BigDecimal emiAmount, + final List disbursementDatas, final BigDecimal maxOutstandingBalance, + final List emiAmountVariations) { return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, - graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance); + graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, + maxOutstandingBalance,emiAmountVariations); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate, - final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail) { + final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, + final boolean multiDisburseLoan,final BigDecimal emiAmount,final List disbursementDatas, + final BigDecimal maxOutstandingBalance,final List emiAmountVariations) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -131,7 +150,8 @@ public final class LoanApplicationTerms { repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, - graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance); + graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, + emiAmount, disbursementDatas, maxOutstandingBalance,emiAmountVariations); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -142,7 +162,9 @@ public final class LoanApplicationTerms { final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Money principal, final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate, final Integer principalGrace, final Integer interestPaymentGrace, - final Integer interestChargingGrace, final LocalDate interestChargedFromDate, final Money inArrearsTolerance) { + final Integer interestChargingGrace, final LocalDate interestChargedFromDate, final Money inArrearsTolerance, + final boolean multiDisburseLoan,final BigDecimal emiAmount,final List disbursementDatas, + final BigDecimal maxOutstandingBalance,final List emiAmountVariations) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; @@ -168,6 +190,11 @@ public final class LoanApplicationTerms { this.interestChargedFromDate = interestChargedFromDate; this.inArrearsTolerance = inArrearsTolerance; + this.multiDisburseLoan = multiDisburseLoan; + this.fixedEmiAmount = emiAmount; + this.disbursementDatas = disbursementDatas; + this.maxOutstandingBalance = maxOutstandingBalance; + this.emiAmountVariations = emiAmountVariations; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -180,6 +207,12 @@ public final class LoanApplicationTerms { // paid too much principal, subtract amount that overpays from // principal paid for period. adjusted = principalForPeriod.minus(totalPrincipalRemaining.abs()); + } else if(multiDisburseLoan){ + final Money difference = this.principal.minus(totalCumulativePrincipalToDate); + final Money halfPrincipalVal = principalForPeriod.dividedBy(2, RoundingMode.HALF_DOWN); + if(difference.isLessThan(halfPrincipalVal)){ + adjusted = principalForPeriod.plus(difference.abs()); + } } else if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { final Money difference = totalCumulativePrincipalToDate.minus(this.principal); @@ -269,10 +302,8 @@ public final class LoanApplicationTerms { // equal installments final int periodsElapsed = periodNumber - 1; final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc); - - final Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod( - periodicInterestRateForRepaymentPeriod, outstandingBalance, periodsElapsed); - + Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod( + periodicInterestRateForRepaymentPeriod, outstandingBalance, periodsElapsed); principalForInstallment = calculatePrincipalDueForInstallment(calculator, mc, periodNumber, totalPmtForThisInstallment, daysInPeriod, outstandingBalance); break; @@ -605,6 +636,10 @@ public final class LoanApplicationTerms { } private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) { + + if(multiDisburseLoan && fixedEmiAmount != null){ + return fixedEmiAmount.doubleValue(); + } final double futureValue = 0; final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); @@ -751,6 +786,10 @@ public final class LoanApplicationTerms { return dateValue; } + public void setPrincipal(Money principal){ + this.principal = principal; + } + public LocalDate getInterestChargedFromLocalDate() { return this.interestChargedFromDate; } @@ -786,4 +825,35 @@ public final class LoanApplicationTerms { public Money getPrincipal() { return this.principal; } + + + public List getDisbursementDatas() { + return this.disbursementDatas; + } + + + public boolean isMultiDisburseLoan() { + return this.multiDisburseLoan; + } + + + public Money getMaxOutstandingBalance() { + return Money.of(getCurrency(), this.maxOutstandingBalance); + } + + + public BigDecimal getFixedEmiAmount() { + return this.fixedEmiAmount; + } + + public void setFixedEmiAmountForPeriod(LocalDate periodDate){ + LocalDate startDate = expectedDisbursementDate; + for(LoanTermVariationsData loanVariationTermsData: this.emiAmountVariations){ + if(!periodDate.isBefore(loanVariationTermsData.getTermApplicableFrom()) + && !startDate.isAfter(loanVariationTermsData.getTermApplicableFrom())){ + this.fixedEmiAmount = loanVariationTermsData.getTermValue(); + startDate = loanVariationTermsData.getTermApplicableFrom(); + } + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java index 61429b3ac..dbcab18b3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java @@ -18,7 +18,7 @@ import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePer public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleModelPeriod { @SuppressWarnings("unused") - private final int periodNumber; + private final Integer periodNumber; private final LocalDate disbursementDate; private final Money principalDisbursed; private final BigDecimal chargesDueAtTimeOfDisbursement; @@ -27,16 +27,24 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo final BigDecimal chargesDueAtTimeOfDisbursement) { final int periodNumber = 0; - return new LoanScheduleModelDisbursementPeriod(periodNumber, loanApplicationTerms, chargesDueAtTimeOfDisbursement); + return new LoanScheduleModelDisbursementPeriod(periodNumber, loanApplicationTerms.getExpectedDisbursementDate(), + loanApplicationTerms.getPrincipal(), chargesDueAtTimeOfDisbursement); + } + + public static LoanScheduleModelDisbursementPeriod disbursement(final LocalDate disbursementDate,final Money principalDisbursed, + final BigDecimal chargesDueAtTimeOfDisbursement){ + return new LoanScheduleModelDisbursementPeriod(null, disbursementDate,principalDisbursed, chargesDueAtTimeOfDisbursement); } - private LoanScheduleModelDisbursementPeriod(final int periodNumber, final LoanApplicationTerms loanApplicationTerms, + private LoanScheduleModelDisbursementPeriod(final Integer periodNumber,final LocalDate disbursementDate,final Money principalDisbursed, final BigDecimal chargesDueAtTimeOfDisbursement) { this.periodNumber = periodNumber; - this.disbursementDate = loanApplicationTerms.getExpectedDisbursementDate(); - this.principalDisbursed = loanApplicationTerms.getPrincipal(); + this.disbursementDate = disbursementDate; + this.principalDisbursed = principalDisbursed; this.chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement; } + + @Override public LoanSchedulePeriodData toData() { @@ -83,4 +91,9 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo public BigDecimal penaltyChargesDue() { return null; } + + @Override + public void addLoanCharges(@SuppressWarnings("unused") BigDecimal feeCharge, @SuppressWarnings("unused") BigDecimal penaltyCharge) { + return; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java index 5d7780033..45d42cd57 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java @@ -29,4 +29,6 @@ public interface LoanScheduleModelPeriod { BigDecimal feeChargesDue(); BigDecimal penaltyChargesDue(); + + void addLoanCharges(BigDecimal feeCharge,BigDecimal penaltyCharge); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java index b649da90f..9c8b1966c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -23,9 +23,9 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel private final Money principalDue; private final Money outstandingLoanBalance; private final Money interestDue; - private final Money feeChargesDue; - private final Money penaltyChargesDue; - private final Money totalDue; + private Money feeChargesDue; + private Money penaltyChargesDue; + private Money totalDue; public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate, final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, @@ -34,7 +34,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance, interestDue, feeChargesDue, penaltyChargesDue, totalDue); } - + public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue) { @@ -115,4 +115,11 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel return value; } + + @Override + public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) { + this.feeChargesDue = this.feeChargesDue.plus(feeCharge); + this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge); + this.totalDue = this.totalDue.plus(feeCharge).plus(penaltyCharge); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index 4c6e4a76c..1a8010cc0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -14,9 +14,14 @@ import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface ScheduledDateGenerator { - List generate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List holidays, + LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, List holidays, final WorkingDays workingDays); LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, - List scheduledDates); + final LocalDate firstRepaymentDate); + + LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms); + + LocalDate adjustRepaymentDate(LocalDate dueRepaymentPeriodDate, LoanApplicationTerms loanApplicationTerms, boolean isHolidayEnabled, + List holidays, WorkingDays workingDays); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementDisbursementDateException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementDisbursementDateException.java new file mode 100644 index 000000000..98d6dee18 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementDisbursementDateException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.loanaccount.loanschedule.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class MultiDisbursementDisbursementDateException extends AbstractPlatformDomainRuleException { + + public MultiDisbursementDisbursementDateException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loanschedule.out.of.schedule.dusbursement.date", defaultUserMessage, defaultUserMessageArgs); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementEmiAmountException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementEmiAmountException.java new file mode 100644 index 000000000..946ab2dde --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementEmiAmountException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.loanaccount.loanschedule.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class MultiDisbursementEmiAmountException extends AbstractPlatformDomainRuleException { + + public MultiDisbursementEmiAmountException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loanschedule.emi.amount.must.be.greter.than.interest", defaultUserMessage, defaultUserMessageArgs); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementOutstandingAmoutException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementOutstandingAmoutException.java new file mode 100644 index 000000000..db8b2a7c7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/exception/MultiDisbursementOutstandingAmoutException.java @@ -0,0 +1,11 @@ +package org.mifosplatform.portfolio.loanaccount.loanschedule.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class MultiDisbursementOutstandingAmoutException extends AbstractPlatformDomainRuleException { + + public MultiDisbursementOutstandingAmoutException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loanschedule.exceeding.max.outstanding.balance", defaultUserMessage, defaultUserMessageArgs); + } + +} 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 5fb77f88c..e4464d319 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -8,9 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Set; +import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; @@ -34,7 +37,12 @@ import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.group.domain.GroupRepositoryWrapper; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariations; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; @@ -50,10 +58,13 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; +import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; @Service public class LoanScheduleAssembler { @@ -188,13 +199,54 @@ public class LoanScheduleAssembler { // other final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance); + + final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); + final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); + + final List disbursementDatas = fetchDisbursementData(element.getAsJsonObject()); + + final List loanVariationTermsData = new ArrayList(); + LoanTermVariationsData data = new LoanTermVariationsData(null, LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), expectedDisbursementDate, emiAmount); + loanVariationTermsData.add(data); return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, - graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); + graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney,loanProduct.isMultiDisburseLoan(), + emiAmount,disbursementDatas, maxOutstandingBalance,loanVariationTermsData); } + + private List fetchDisbursementData(final JsonObject command) { + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command); + List disbursementDatas = new ArrayList(); + if (command.has(LoanApiConstants.disbursementDataParameterName)) { + final JsonArray disbursementDataArray = command.getAsJsonArray(LoanApiConstants.disbursementDataParameterName); + if (disbursementDataArray != null && disbursementDataArray.size() > 0) { + int i = 0; + do { + final JsonObject jsonObject = disbursementDataArray.get(i).getAsJsonObject(); + LocalDate expectedDisbursementDate = null; + BigDecimal principal = null; + + if (jsonObject.has(LoanApiConstants.disbursementDateParameterName)) { + expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.disbursementDateParameterName, jsonObject,dateFormat,locale); + } + if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName) + && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() + && StringUtils.isNotBlank((jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) { + principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal(); + } + disbursementDatas.add(new DisbursementData(null, expectedDisbursementDate, null, principal,null)); + i++; + } while (i < disbursementDataArray.size()); + } + } + return disbursementDatas; + } + + private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) { if (repaymentsStartingFromDate != null diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 1880ab1e3..5fa159c33 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -22,6 +22,7 @@ import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -42,7 +43,8 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { "loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", - "dateFormat", "charges", "collateral", "syncDisbursementWithMeeting", "linkAccountId")); + "dateFormat", "charges", "collateral", "syncDisbursementWithMeeting", "linkAccountId",LoanApiConstants.disbursementDataParameterName, + LoanApiConstants.emiAmountParameterName,LoanApiConstants.maxOutstandingBalanceParameterName)); private final FromJsonHelper fromApiJsonHelper; 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 3fc5de7ae..b53561b36 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 @@ -23,7 +23,9 @@ import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.portfolio.accountdetails.domain.AccountType; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; import org.mifosplatform.portfolio.loanaccount.domain.Loan; +import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.savings.domain.SavingsAccount; import org.springframework.beans.factory.annotation.Autowired; @@ -51,7 +53,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { "transactionProcessingStrategyId", // settings "calendarId", // optional "syncDisbursementWithMeeting",// optional - "linkAccountId")); + "linkAccountId",LoanApiConstants.disbursementDataParameterName,LoanApiConstants.emiAmountParameterName, + LoanApiConstants.maxOutstandingBalanceParameterName)); private final FromJsonHelper fromApiJsonHelper; private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper; @@ -331,6 +334,16 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero(); } + if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)){ + final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount(); + } + if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)){ + final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance).ignoreIfNull().positiveAmount(); + } + validateLoanMultiDisbursementdate(element, baseDataValidator, expectedDisbursementDate, principal); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } @@ -404,17 +417,18 @@ public final class LoanApplicationCommandFromApiJsonHelper { } final String principalParameterName = "principal"; + BigDecimal principal = null; if (this.fromApiJsonHelper.parameterExists(principalParameterName, element)) { atLeastOneParameterPassedForUpdate = true; - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalParameterName, element); + principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalParameterName, element); baseDataValidator.reset().parameter(principalParameterName).value(principal).notNull().positiveAmount(); } final String inArrearsToleranceParameterName = "inArrearsTolerance"; if (this.fromApiJsonHelper.parameterExists(inArrearsToleranceParameterName, element)) { atLeastOneParameterPassedForUpdate = true; - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(inArrearsToleranceParameterName, element); - baseDataValidator.reset().parameter(inArrearsToleranceParameterName).value(principal).ignoreIfNull().positiveAmount(); + final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(inArrearsToleranceParameterName, element); + baseDataValidator.reset().parameter(inArrearsToleranceParameterName).value(inArrearsTolerance).ignoreIfNull().positiveAmount(); } final String loanTermFrequencyParameterName = "loanTermFrequency"; @@ -490,6 +504,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { } final String expectedDisbursementDateParameterName = "expectedDisbursementDate"; + LocalDate expectedDisbursementDate = null; if (this.fromApiJsonHelper.parameterExists(expectedDisbursementDateParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -497,7 +512,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { element); baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDateStr).notBlank(); - final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(expectedDisbursementDateParameterName, + expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(expectedDisbursementDateParameterName, element); baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDate).notNull(); } @@ -650,6 +665,19 @@ public final class LoanApplicationCommandFromApiJsonHelper { final Object forceError = null; baseDataValidator.reset().anyOfNotNull(forceError); } + + if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)){ + final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount(); + } + + if(this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)){ + final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance).ignoreIfNull().positiveAmount(); + } + + validateLoanMultiDisbursementdate(element, baseDataValidator, expectedDisbursementDate, principal); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } @@ -719,5 +747,57 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } + + + private void validateLoanMultiDisbursementdate(final JsonElement element, final DataValidatorBuilder baseDataValidator,LocalDate expectedDisbursement, + BigDecimal totalPrincipal){ + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.disbursementDataParameterName, element) && expectedDisbursement !=null + && totalPrincipal !=null) { + BigDecimal tatalDisbursement = BigDecimal.ZERO; + boolean isFirstinstallmentOnExpectedDisbursementDate = false; + final JsonArray variationArray= this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element); + List expectedDisbursementDates = new ArrayList(); + if (variationArray != null && variationArray.size() > 0) { + int i = 0; + do { + final JsonObject jsonObject = variationArray.get(i).getAsJsonObject(); + + BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalNamed( + LoanApiConstants.disbursementPrincipalParameterName, jsonObject,locale);; + + LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.disbursementDateParameterName, + jsonObject,dateFormat,locale); + if(expectedDisbursementDates.contains(expectedDisbursementDate)){ + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDateParameterName).failWithCode(LoanApiConstants.DISBURSEMENT_DATE_UNIQUE_ERROR); + } + expectedDisbursementDates.add(expectedDisbursementDate); + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDataParameterName).parameterAtIndexArray(LoanApiConstants.disbursementPrincipalParameterName, i).value(principal).notBlank(); + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDataParameterName).parameterAtIndexArray(LoanApiConstants.disbursementDateParameterName, i).value(expectedDisbursementDate).notNull(); + if(principal != null){ + tatalDisbursement = tatalDisbursement.add(principal); + } + if(expectedDisbursement.equals(expectedDisbursementDate)){ + isFirstinstallmentOnExpectedDisbursementDate = true; + } + i++; + } while (i < variationArray.size()); + if(!isFirstinstallmentOnExpectedDisbursementDate){ + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDateParameterName).failWithCode(LoanApiConstants.DISBURSEMENT_DATE_START_WITH_ERROR); + } + if(!tatalDisbursement.equals(totalPrincipal)){ + baseDataValidator.reset().parameter(LoanApiConstants.disbursementPrincipalParameterName).failWithCode(LoanApiConstants.PRINCIPAL_AMOUNT_SHOULD_BE_SAME); + } + final String interestTypeParameterName = "interestType"; + final Integer interestType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(interestTypeParameterName, element); + baseDataValidator.reset().parameter(interestTypeParameterName).value(interestType).ignoreIfNull().integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue()); + } + + } + + + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index a1131200f..ea9d93bc4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -25,6 +25,8 @@ import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException; import org.mifosplatform.portfolio.calendar.service.CalendarUtils; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; +import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -51,7 +53,8 @@ public final class LoanEventApiJsonValidator { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } final Set disbursementParameters = new HashSet(Arrays.asList("actualDisbursementDate", "externalId", "note", - "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber")); + "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", + LoanApiConstants.principalDisbursedParameterName,LoanApiConstants.emiAmountParameterName)); final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); @@ -65,6 +68,12 @@ public final class LoanEventApiJsonValidator { final String note = this.fromApiJsonHelper.extractStringNamed("note", element); baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalDisbursedParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.principalDisbursedParameterName).value(principal).ignoreIfNull().positiveAmount(); + + final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAmount).ignoreIfNull().positiveAmount().notGreaterThanMax(principal); validatePaymentDetails(baseDataValidator, element); @@ -340,5 +349,29 @@ public final class LoanEventApiJsonValidator { baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); throwExceptionIfValidationWarningsExist(dataValidationErrors); } + + public void validateUpdateDisbursementDate(final String json,LoanDisbursementDetails loanDisbursementDetails) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Set disbursementParameters = new HashSet(Arrays.asList(LoanApiConstants.disbursementDateParameterName,"locale", "dateFormat")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.update.disbursement"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.disbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDateParameterName).value(actualDisbursementDate).notNull(); + + if(loanDisbursementDetails.actualDisbursementDate() != null){ + baseDataValidator.reset().parameter(LoanApiConstants.disbursementDateParameterName).failWithCode(LoanApiConstants.ALREADY_DISBURSED); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index 30bd3e713..8af15483a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -5,10 +5,15 @@ */ package org.mifosplatform.portfolio.loanaccount.service; +import java.math.BigDecimal; import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; +import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper; @@ -39,16 +44,20 @@ import org.mifosplatform.portfolio.group.domain.GroupRepository; import org.mifosplatform.portfolio.group.exception.ClientNotInGroupException; import org.mifosplatform.portfolio.group.exception.GroupNotActiveException; import org.mifosplatform.portfolio.group.exception.GroupNotFoundException; +import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants; import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository; +import org.mifosplatform.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException; +import org.mifosplatform.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; @@ -63,7 +72,9 @@ import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; @Service public class LoanAssembler { @@ -178,13 +189,33 @@ public class LoanAssembler { final String loanTypeParameterName = "loanType"; final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); final EnumOptionData loanType = AccountEnumerations.loanType(loanTypeStr); + Set disbursementDetails = null; + BigDecimal fixedEmiAmount = null; + BigDecimal maxOutstandingLoanBalance = null; + if(loanProduct.isMultiDisburseLoan()){ + disbursementDetails = fetchDisbursementData(element.getAsJsonObject()); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject()); + fixedEmiAmount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.emiAmountParameterName, element, locale); + maxOutstandingLoanBalance = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element, locale); + if(disbursementDetails.isEmpty()){ + final String errorMessage = "For this loan product, disbursement details must be provided"; + throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + + if(disbursementDetails.size() > loanProduct.maxTrancheCount()){ + final String errorMessage = "Number of tranche shouldn't be greter than "+loanProduct.maxTrancheCount(); + throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, + loanProduct.maxTrancheCount(),disbursementDetails.size()); + } + } if (clientId != null) { client = this.clientRepository.findOneWithNotFoundDetection(clientId); if (client.isNotActive()) { throw new ClientNotActiveException(clientId); } loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, - loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral); + loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, + fixedEmiAmount, disbursementDetails,maxOutstandingLoanBalance); } if (groupId != null) { @@ -193,7 +224,8 @@ public class LoanAssembler { if (group.isNotActive()) { throw new GroupNotActiveException(groupId); } loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting); + loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount, + disbursementDetails, maxOutstandingLoanBalance); } if (client != null && group != null) { @@ -202,9 +234,9 @@ public class LoanAssembler { loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, - syncDisbursementWithMeeting); + syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,maxOutstandingLoanBalance); } - + final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element); final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element); @@ -212,6 +244,12 @@ public class LoanAssembler { loanApplication.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); + if(loanProduct.isMultiDisburseLoan()){ + for(final LoanDisbursementDetails loanDisbursementDetails:loanApplication.getDisbursementDetails()){ + loanDisbursementDetails.updateLoan(loanApplication); + } + } + final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), @@ -227,6 +265,40 @@ public class LoanAssembler { return loanApplication; } + + private Set fetchDisbursementData(final JsonObject command) { + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command); + Set disbursementDatas = new HashSet(); + if (command.has(LoanApiConstants.disbursementDataParameterName)) { + final JsonArray disbursementDataArray = command.getAsJsonArray(LoanApiConstants.disbursementDataParameterName); + if (disbursementDataArray != null && disbursementDataArray.size() > 0) { + int i = 0; + do { + final JsonObject jsonObject = disbursementDataArray.get(i).getAsJsonObject(); + Date expectedDisbursementDate = null; + BigDecimal principal = null; + + if (jsonObject.has(LoanApiConstants.disbursementDateParameterName)) { + LocalDate date = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.disbursementDateParameterName, jsonObject,dateFormat,locale); + if(date!=null){ + expectedDisbursementDate = date.toDate(); + } + } + if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName) + && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() + && StringUtils.isNotBlank((jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) { + principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal(); + } + disbursementDatas.add(new LoanDisbursementDetails(expectedDisbursementDate, null, principal)); + i++; + } while (i < disbursementDataArray.size()); + } + } + return disbursementDatas; + } + + private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { final List allowedLoanStatuses = Arrays.asList(LoanStatus.values()); return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java index f4321cb57..9afe0d543 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -11,7 +11,9 @@ import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.organisation.staff.data.StaffData; import org.mifosplatform.portfolio.calendar.data.CalendarData; import org.mifosplatform.portfolio.group.service.SearchParameters; +import org.mifosplatform.portfolio.loanaccount.data.DisbursementData; import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; @@ -21,7 +23,7 @@ public interface LoanReadPlatformService { LoanAccountData retrieveOne(Long loanId); - LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData); + LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData, Collection disbursementData); Collection retrieveLoanTransactions(Long loanId); @@ -65,4 +67,10 @@ public interface LoanReadPlatformService { Integer retriveLoanCounter(Long groupId, Integer loanType, Long productId); Integer retriveLoanCounter(Long clientId, Long productId); + + Collection retrieveLoanDisbursementDetails(Long loanId); + + DisbursementData retrieveLoanDisbursementDetail(Long loanId,Long disbursementId); + + Collection retrieveLoanTermVariations(Long loanId,Integer termType); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 7a1fc1362..014bb1561 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -54,11 +54,13 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData; import org.mifosplatform.portfolio.loanaccount.data.LoanApplicationTimelineData; import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData; import org.mifosplatform.portfolio.loanaccount.data.LoanSummaryData; +import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; @@ -155,13 +157,13 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @Override public LoanScheduleData retrieveRepaymentSchedule(final Long loanId, - final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData) { + final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection disbursementData) { try { this.context.authenticatedUser(); final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor( - repaymentScheduleRelatedLoanData); + repaymentScheduleRelatedLoanData, disbursementData); final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment"; return this.jdbcTemplate.query(sql, fullResultsetExtractor, new Object[] { loanId }); @@ -340,7 +342,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Collection paymentOptions = this.codeValueReadPlatformService .retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, - possibleNextRepaymentAmount.getAmount(), null, null, null, null, null, paymentOptions, null, null); + possibleNextRepaymentAmount.getAmount(), null, null, null, null, null, paymentOptions, null, null,null); } @Override @@ -364,7 +366,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final BigDecimal amount = waiveOfInterest.getAmount(currency).getAmount(); return new LoanTransactionData(null, null, null, transactionType, null, currencyData, waiveOfInterest.getTransactionDate(), amount, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null); } @Override @@ -374,7 +376,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WRITEOFF); return new LoanTransactionData(null, null, null, transactionType, null, null, DateUtils.getLocalDateOfTenant(), null, null, null, - null, null, null, null, null); + null, null, null, null, null, null); } @Override @@ -384,8 +386,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.DISBURSEMENT); final Collection paymentOptions = this.codeValueReadPlatformService .retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); - return new LoanTransactionData(null, null, null, transactionType, null, null, loan.getExpectedDisbursedOnLocalDate(), null, null, - null, null, null, null, paymentOptions, null, null); + return new LoanTransactionData(null, null, null, transactionType, null, null, loan.getExpectedDisbursedOnLocalDateForTemplate(), loan.getDisburseAmountForTemplate(), + null,null, null, null, null, paymentOptions, null, null,loan.retriveLastEmiAmount()); } @Override @@ -417,6 +419,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { return "l.id as id, l.account_no as accountNo, l.external_id as externalId, l.fund_id as fundId, f.name as fundName," + " l.loan_type_enum as loanType, l.loanpurpose_cv_id as loanPurposeId, cv.code_value as loanPurposeName," + " lp.id as loanProductId, lp.name as loanProductName, lp.description as loanProductDescription," + + " lp.multi_disburse_loan as multiDisburseLoan," + " c.id as clientId, c.display_name as clientName, c.office_id as clientOfficeId," + " g.id as groupId, g.display_name as groupName," + " g.office_id as groupOfficeId, g.staff_id As groupStaffId , g.parent_id as groupParentId, " @@ -429,7 +432,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname," + " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, " + " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.expected_maturedon_date as expectedMaturityDate, " - + " l.principal_amount as principal, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + + " l.principal_amount as principal,l.approved_principal as approvedPrincipal, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + " l.grace_on_principal_periods as graceOnPrincipalPayment, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged," + " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, " + " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, " @@ -467,6 +470,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.total_writtenoff_derived as totalWrittenOff," + " l.total_outstanding_derived as totalOutstanding," + " l.total_overpaid_derived as totalOverpaid," + + " l.fixed_emi_amount as fixedEmiAmount," + + " l.max_outstanding_loan_balance as outstandingLoanBalance," + " la.principal_overdue_derived as principalOverdue," + " la.interest_overdue_derived as interestOverdue," + " la.fee_charges_overdue_derived as feeChargesOverdue," @@ -536,6 +541,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Long loanProductId = JdbcSupport.getLong(rs, "loanProductId"); final String loanProductName = rs.getString("loanProductName"); final String loanProductDescription = rs.getString("loanProductDescription"); + final Boolean multiDisburseLoan = rs.getBoolean("multiDisburseLoan"); + final BigDecimal outstandingLoanBalance = rs.getBigDecimal("outstandingLoanBalance"); final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); final String submittedByUsername = rs.getString("submittedByUsername"); @@ -580,6 +587,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { expectedMaturityDate, writtenOffOnDate, closedByUsername, closedByFirstname, closedByLastname); final BigDecimal principal = rs.getBigDecimal("principal"); + final BigDecimal approvedPrincipal = rs.getBigDecimal("approvedPrincipal"); final BigDecimal totalOverpaid = rs.getBigDecimal("totalOverpaid"); final BigDecimal inArrearsTolerance = rs.getBigDecimal("inArrearsTolerance"); @@ -687,15 +695,17 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Integer loanCounter = JdbcSupport.getInteger(rs, "loanCounter"); final Integer loanProductCounter = JdbcSupport.getInteger(rs, "loanProductCounter"); + final BigDecimal fixedEmiAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "fixedEmiAmount"); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, - loanOfficerId, loanOfficerName, currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, + loanOfficerId, loanOfficerName, currencyData, principal,approvedPrincipal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, transactionStrategyName, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, - feeChargesDueAtDisbursementCharged, syncDisbursementWithMeeting, loanCounter, loanProductCounter); + feeChargesDueAtDisbursementCharged, syncDisbursementWithMeeting, loanCounter, loanProductCounter, multiDisburseLoan, + fixedEmiAmount,outstandingLoanBalance); } } @@ -737,15 +747,17 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final CurrencyData currency; private final DisbursementData disbursement; private final BigDecimal totalFeeChargesDueAtDisbursement; + private final Collection disbursementData; private LocalDate lastDueDate; private BigDecimal outstandingLoanPrincipalBalance; - public LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData) { + public LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection disbursementData) { this.currency = repaymentScheduleRelatedLoanData.getCurrency(); this.disbursement = repaymentScheduleRelatedLoanData.disbursementData(); this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement(); this.lastDueDate = this.disbursement.disbursementDate(); this.outstandingLoanPrincipalBalance = this.disbursement.amount(); + this.disbursementData = disbursementData; } public String schema() { @@ -767,11 +779,17 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { this.disbursement.isDisbursed()); final Collection periods = new ArrayList(); - periods.add(disbursementPeriod); - final MonetaryCurrency monCurrency = new MonetaryCurrency(this.currency.code(), this.currency.decimalPlaces(), this.currency.currencyInMultiplesOf()); - final BigDecimal totalPrincipalDisbursed = Money.of(monCurrency, this.disbursement.amount()).getAmount(); + BigDecimal totalPrincipalDisbursed = BigDecimal.ZERO; + if(disbursementData == null || disbursementData.isEmpty()){ + periods.add(disbursementPeriod); + totalPrincipalDisbursed = Money.of(monCurrency, this.disbursement.amount()).getAmount(); + }else{ + this.outstandingLoanPrincipalBalance = BigDecimal.ZERO; + } + + Money totalPrincipalExpected = Money.zero(monCurrency); Money totalPrincipalPaid = Money.zero(monCurrency); Money totalInterestCharged = Money.zero(monCurrency); @@ -800,6 +818,28 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate"); final boolean complete = rs.getBoolean("complete"); + + if(disbursementData!=null){ + BigDecimal principal = BigDecimal.ZERO; + for(DisbursementData data:disbursementData){ + if(fromDate.equals(this.disbursement.disbursementDate()) + && data.disbursementDate().equals(fromDate)){ + principal = principal.add(data.amount()); + final LoanSchedulePeriodData periodData = LoanSchedulePeriodData.disbursementOnlyPeriod( + data.disbursementDate(), data.amount(), this.totalFeeChargesDueAtDisbursement, + data.isDisbursed()); + periods.add(periodData); + }else if(data.isDueForDisbursement(fromDate, dueDate)){ + principal = principal.add(data.amount()); + final LoanSchedulePeriodData periodData = LoanSchedulePeriodData.disbursementOnlyPeriod( + data.disbursementDate(), data.amount(), BigDecimal.ZERO, + data.isDisbursed()); + periods.add(periodData); + } + } + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principal); + totalPrincipalDisbursed = totalPrincipalDisbursed.add(principal); + } Integer daysInPeriod = Integer.valueOf(0); if (fromDate != null) { @@ -895,6 +935,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { totalWrittenOff.getAmount(), totalRepaymentExpected.getAmount(), totalRepayment.getAmount(), totalPaidInAdvance.getAmount(), totalPaidLate.getAmount(), totalOutstanding.getAmount()); } + } private static final class LoanTransactionsMapper implements RowMapper { @@ -989,7 +1030,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalAmount, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, externalId, transfer); + principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, externalId, transfer, null); } } @@ -1179,4 +1220,71 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final String sql = "Select MAX(l.loan_product_counter) from m_loan l where l.client_id = ? and l.product_id=?"; return this.jdbcTemplate.queryForInt(sql, clientId,productId); } + + @Override + public Collection retrieveLoanDisbursementDetails(final Long loanId) { + final LoanDisbursementDetailMapper rm = new LoanDisbursementDetailMapper(); + final String sql = "select " + rm.schema() + " where dd.loan_id=? order by dd.expected_disburse_date"; + return this.jdbcTemplate.query(sql, rm, new Object[] { loanId }); + } + + + private static final class LoanDisbursementDetailMapper implements RowMapper { + + public String schema() { + return "dd.id as id,dd.expected_disburse_date as expectedDisbursementdate,dd.disbursedon_date as actualDisbursementdate,dd.principal as principal,dd.approved_principal as approvedPrincipal " + + "from m_loan_disbursement_detail dd"; + } + + @Override + public DisbursementData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final Long id = rs.getLong("id"); + final LocalDate expectedDisbursementdate = JdbcSupport.getLocalDate(rs, "expectedDisbursementdate"); + final LocalDate actualDisbursementdate = JdbcSupport.getLocalDate(rs, "actualDisbursementdate"); + final BigDecimal principal = rs.getBigDecimal("principal"); + final BigDecimal approvedPrincipal = rs.getBigDecimal("approvedPrincipal"); + + final DisbursementData disbursementData = new DisbursementData(id,expectedDisbursementdate,actualDisbursementdate,principal,approvedPrincipal); + return disbursementData; + } + + } + + @Override + public DisbursementData retrieveLoanDisbursementDetail(Long loanId,Long disbursementId) { + final LoanDisbursementDetailMapper rm = new LoanDisbursementDetailMapper(); + final String sql = "select " + rm.schema() + " where dd.loan_id=? and dd.id=?"; + return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { loanId,disbursementId }); + } + + @Override + public Collection retrieveLoanTermVariations(Long loanId, Integer termType) { + final LoanTermVariationsMapper rm = new LoanTermVariationsMapper(); + final String sql = "select " + rm.schema() + " where tv.loan_id=? and tv.term_type=?"; + return this.jdbcTemplate.query(sql, rm, new Object[] { loanId,termType }); + } + + private static final class LoanTermVariationsMapper implements RowMapper { + + public String schema() { + return "tv.id as id,tv.applicable_from as variationApplicableFrom,tv.term_value as termValue " + + "from m_loan_term_variations tv"; + } + + @Override + public LoanTermVariationsData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final Long id = rs.getLong("id"); + final LocalDate variationApplicableFrom = JdbcSupport.getLocalDate(rs, "variationApplicableFrom"); + final BigDecimal termValue = rs.getBigDecimal("termValue"); + + final LoanTermVariationsData loanTermVariationsData = new LoanTermVariationsData(id,LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), + variationApplicableFrom,termValue); + return loanTermVariationsData; + } + + } + + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java index 5bde6dce9..ae037d044 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -76,4 +76,6 @@ public interface LoanWritePlatformService { CommandProcessingResult undoWriteOff(Long loanId); + CommandProcessingResult updateDisbursementDateForTranche(Long loanId, Long disbursementId, JsonCommand command); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index f4a45eb56..f660abcee 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -47,6 +47,8 @@ import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWr import org.mifosplatform.portfolio.account.PortfolioAccountType; import org.mifosplatform.portfolio.account.data.AccountTransferDTO; import org.mifosplatform.portfolio.account.data.PortfolioAccountData; +import org.mifosplatform.portfolio.account.domain.AccountTransfer; +import org.mifosplatform.portfolio.account.domain.AccountTransferRepository; import org.mifosplatform.portfolio.account.service.AccountAssociationsReadPlatformService; import org.mifosplatform.portfolio.account.service.AccountTransfersReadPlatformService; import org.mifosplatform.portfolio.account.service.AccountTransfersWritePlatformService; @@ -85,6 +87,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository; +import org.mifosplatform.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; @@ -151,6 +154,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanChargeReadPlatformService LoanChargeReadPlatformService; private final LoanReadPlatformService loanReadPlatformService; private final FromJsonHelper fromApiJsonHelper; + private final AccountTransferRepository accountTransferRepository; @Autowired public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -169,7 +173,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, final LoanChargeReadPlatformService loanChargeReadPlatformService, final LoanReadPlatformService loanReadPlatformService, - final FromJsonHelper fromApiJsonHelper) { + final FromJsonHelper fromApiJsonHelper,final AccountTransferRepository accountTransferRepository) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanAssembler = loanAssembler; @@ -195,6 +199,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.LoanChargeReadPlatformService = loanChargeReadPlatformService; this.loanReadPlatformService = loanReadPlatformService; this.fromApiJsonHelper = fromApiJsonHelper; + this.accountTransferRepository = accountTransferRepository; } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { @@ -247,12 +252,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); updateLoanCounters(loan, actualDisbursementDate); - loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, - workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, + existingTransactionIds, existingReversedTransactionIds, changes, paymentDetail, calculatedRepaymentsStartingFromDate, isHolidayEnabled, + holidays,workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); if (!changes.isEmpty()) { - try { + try{ this.loanRepository.saveAndFlush(loan); } catch (final DataIntegrityViolationException e) { final Throwable realCause = e.getCause(); @@ -265,11 +270,35 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf "validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } + final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanNote(loan, noteText); this.noteRepository.save(note); } + + //fix me + //adding loan installment charges along with the actual disbursement, is trying to add 2 extra entries with null values. which is causing issues + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionForDisbursement(); + try{ + this.loanRepository.saveAndFlush(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } + } + + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } @@ -277,14 +306,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Set loanCharges = loan.charges(); final Map disBuLoanCharges = new HashMap(); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - disBuLoanCharges.put(loanCharge.getId(), loanCharge.amount()); - } else if (loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()) { - final Set chargePerInstallments = loan.generateInstallmentLoanCharges(loanCharge); - loanCharge.addLoanInstallmentCharges(chargePerInstallments); - this.loanChargeRepository.save(loanCharge); - } + if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer() + && loanCharge.isNotFullyPaid() && !loanCharge.isWaived()) { + disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding()); + } } + final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); for (final Map.Entry entrySet : disBuLoanCharges.entrySet()) { @@ -398,19 +425,38 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Note note = Note.loanNote(loan, noteText); this.noteRepository.save(note); } + + //fix me + //adding loan installment charges along with the actual disbursement, is trying to add 2 extra entries with null values. which is causing issues + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionForDisbursement(); + try{ + this.loanRepository.saveAndFlush(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } + } + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } final Set loanCharges = loan.charges(); final Map disBuLoanCharges = new HashMap(); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - disBuLoanCharges.put(loanCharge.getId(), loanCharge.amount()); - } else if (loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()) { - final Set chargePerInstallments = loan.generateInstallmentLoanCharges(loanCharge); - loanCharge.addLoanInstallmentCharges(chargePerInstallments); - this.loanChargeRepository.save(loanCharge); - } + if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer() + && loanCharge.isNotFullyPaid() && !loanCharge.isWaived()) { + disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding()); + } } final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); @@ -737,12 +783,30 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); - final LoanTransaction writeoff = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(), changes, - existingTransactionIds, existingReversedTransactionIds, currentUser); + + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan, + calendarInstance); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), + loan.getDisbursementDate().toDate()); + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + updateLoanCounters(loan, loan.getDisbursementDate()); + final MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + + final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(), changes, + existingTransactionIds, existingReversedTransactionIds, currentUser, this.loanScheduleFactory, + applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); + LoanTransaction writeoff = changedTransactionDetail.getNewTransactionMappings().remove(0L); this.loanTransactionRepository.save(writeoff); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } this.loanRepository.save(loan); - final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); @@ -782,12 +846,32 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); - final LoanTransaction possibleClosingTransaction = loan.close(command, defaultLoanLifecycleStateMachine(), changes, - existingTransactionIds, existingReversedTransactionIds); + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan, + calendarInstance); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), + loan.getDisbursementDate().toDate()); + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + updateLoanCounters(loan, loan.getDisbursementDate()); + + final MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + + ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine(), changes, + existingTransactionIds, existingReversedTransactionIds,this.loanScheduleFactory, + applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); + final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L); if (possibleClosingTransaction != null) { this.loanTransactionRepository.save(possibleClosingTransaction); } + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); + this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } this.loanRepository.save(loan); + final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -1747,4 +1831,57 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .withLoanId(loanId) // .build(); } + + @Override + @Transactional + public CommandProcessingResult updateDisbursementDateForTranche(final Long loanId,final Long disbursementId, final JsonCommand command){ + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId); + this.loanEventApiJsonValidator.validateUpdateDisbursementDate(command.json(),loanDisbursementDetails); + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan, + calendarInstance); + final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), + loan.getDisbursementDate().toDate()); + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + + final MonetaryCurrency currency = loan.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + final List existingTransactionIds = new ArrayList(); + final List existingReversedTransactionIds = new ArrayList(); + final Map changes = new LinkedHashMap(); + final ChangedTransactionDetail changedTransactionDetail = loan.updateDisbursementDateForTranche(loanDisbursementDetails,command, existingTransactionIds, existingReversedTransactionIds, + changes,this.loanScheduleFactory,applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); + + this.loanRepository.saveAndFlush(loan); + + if (changedTransactionDetail != null) { + for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .with(changes) + .build(); + + } + + private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { + final AccountTransfer transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); + if (transferTransaction != null) { + transferTransaction.updateToLoanTransaction(newLoanTransaction); + this.accountTransferRepository.save(transferTransaction); + } + } } \ No newline at end of file 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 69f819cc9..bba40d795 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 @@ -50,4 +50,9 @@ public interface LoanProductConstants { public static final String VALUE_CONDITION_END_WITH_ERROR = "condition.type.must.end.with.greterthan"; public static final String VALUE_CONDITION_START_WITH_ERROR = "condition.type.must.start.with.equal"; public static final String shortName = "shortName"; + + public static final String multiDisburseLoanParameterName = "multiDisburseLoan"; + public static final String maxTrancheCountParameterName = "maxTrancheCount"; + public static final String outstandingLoanBalanceParameterName = "outstandingLoanBalance"; + } 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 7028badd7..24d8ac99b 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 @@ -106,6 +106,10 @@ public class LoanProductData { private final Map> accountingMappingOptions; @SuppressWarnings("unused") private final List valueConditionTypeOptions; + + private final Boolean multiDisburseLoan; + private final Integer maxTrancheCount; + private final BigDecimal outstandingLoanBalance; /** * Used when returning lookup information about loan product for dropdowns. */ @@ -148,13 +152,16 @@ public class LoanProductData { final LocalDate closeDate = null; final String status = null; final String externalId = null; - return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, + final Boolean multiDisburseLoan = null; + final Integer maxTrancheCount = null; + final BigDecimal outstandingLoanBalance = null; + return new LoanProductData(id, name,shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations, - interestRateVariations, numberOfRepaymentVariations); + interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); } public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) { @@ -197,6 +204,10 @@ public class LoanProductData { final Collection principalVariations = new ArrayList(1); final Collection interestRateVariations = new ArrayList(1); final Collection numberOfRepaymentVariations = new ArrayList(1); + final Boolean multiDisburseLoan = null; + final Integer maxTrancheCount = null; + final BigDecimal outstandingLoanBalance = null; + return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, @@ -204,7 +215,7 @@ public class LoanProductData { interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations, - interestRateVariations, numberOfRepaymentVariations); + interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); } public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { @@ -253,6 +264,10 @@ public class LoanProductData { final LocalDate closeDate = null; final String status = null; final String externalId = null; + final Boolean multiDisburseLoan = null; + final Integer maxTrancheCount = null; + final BigDecimal outstandingLoanBalance = null; + return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, @@ -260,7 +275,7 @@ public class LoanProductData { interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariationsForBorrowerCycle, - interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle); + interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -286,7 +301,8 @@ public class LoanProductData { final Collection charges, final EnumOptionData accountingType, final boolean includeInBorrowerCycle, boolean useBorrowerCycle, final LocalDate startDate, final LocalDate closeDate, final String status, final String externalId, Collection principalVariations, Collection interestRateVariations, - Collection numberOfRepaymentVariations) { + Collection numberOfRepaymentVariations, Boolean multiDisburseLoan, Integer maxTrancheCount, + BigDecimal outstandingLoanBalance) { this.id = id; this.name = name; this.shortName = shortName; @@ -347,6 +363,9 @@ public class LoanProductData { this.principalVariationsForBorrowerCycle = principalVariations; this.interestRateVariationsForBorrowerCycle = interestRateVariations; this.numberOfRepaymentVariationsForBorrowerCycle = numberOfRepaymentVariations; + this.multiDisburseLoan = multiDisburseLoan; + this.outstandingLoanBalance = outstandingLoanBalance; + this.maxTrancheCount = maxTrancheCount; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -419,6 +438,9 @@ public class LoanProductData { this.graceOnInterestCharged = productData.graceOnInterestCharged; this.includeInBorrowerCycle = productData.includeInBorrowerCycle; this.useBorrowerCycle = productData.useBorrowerCycle; + this.multiDisburseLoan = productData.multiDisburseLoan; + this.maxTrancheCount = productData.maxTrancheCount; + this.outstandingLoanBalance = productData.outstandingLoanBalance; this.amortizationTypeOptions = amortizationTypeOptions; this.interestTypeOptions = interestTypeOptions; @@ -610,4 +632,14 @@ public class LoanProductData { public Collection getNumberOfRepaymentVariationsForBorrowerCycle() { return this.numberOfRepaymentVariationsForBorrowerCycle; } + + + public Boolean getMultiDisburseLoan() { + return this.multiDisburseLoan; + } + + + public BigDecimal getOutstandingLoanBalance() { + return this.outstandingLoanBalance; + } } \ No newline at end of file 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 729785598..b850e51ef 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 @@ -96,6 +96,9 @@ public class LoanProduct extends AbstractPersistable { @Column(name = "use_borrower_cycle") private boolean useBorrowerCycle; + + @Embedded + private LoanProductTrancheDetails loanProducTrancheDetails; @Column(name = "start_date", nullable = true) @Temporal(TemporalType.DATE) @@ -165,12 +168,22 @@ public class LoanProduct extends AbstractPersistable { populateBorrowerCyclevariations(command, loanProductBorrowerCycleVariations); } + + final boolean multiDisburseLoan = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.multiDisburseLoanParameterName); + Integer maxTrancheCount = null; + BigDecimal outstandingLoanBalance = null; + if(multiDisburseLoan){ + outstandingLoanBalance = command.bigDecimalValueOfParameterNamed(LoanProductConstants.outstandingLoanBalanceParameterName); + maxTrancheCount = command.integerValueOfParameterNamed(LoanProductConstants.maxTrancheCountParameterName); + } + + return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentFrequencyType, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle, startDate, closeDate, - externalId, useBorrowerCycle, loanProductBorrowerCycleVariations); + externalId, useBorrowerCycle, loanProductBorrowerCycleVariations,multiDisburseLoan,maxTrancheCount,outstandingLoanBalance); } /** @@ -379,7 +392,8 @@ public class LoanProduct extends AbstractPersistable { final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final List charges, final AccountingRuleType accountingRuleType, final boolean includeInBorrowerCycle, final LocalDate startDate, final LocalDate closeDate, final String externalId, final boolean useBorrowerCycle, - final Set loanProductBorrowerCycleVariations) { + final Set loanProductBorrowerCycleVariations, + final boolean multiDisburseLoan,final Integer maxTrancheCount, final BigDecimal outstandingLoanBalance) { this.fund = fund; this.transactionProcessingStrategy = transactionProcessingStrategy; this.name = name.trim(); @@ -424,6 +438,7 @@ public class LoanProduct extends AbstractPersistable { for (LoanProductBorrowerCycleVariations borrowerCycleVariations : this.borrowerCycleVariations) { borrowerCycleVariations.updateLoanProduct(this); } + this.loanProducTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); } public MonetaryCurrency getCurrency() { @@ -586,10 +601,12 @@ public class LoanProduct extends AbstractPersistable { actualChanges.put(accountingTypeParamName, newValue); this.externalId = newValue; } + loanProducTrancheDetails.update(command, actualChanges, localeAsInput); return actualChanges; } + public boolean isCashBasedAccountingEnabled() { return AccountingRuleType.CASH_BASED.getValue().equals(this.accountingRule); } @@ -678,6 +695,18 @@ public class LoanProduct extends AbstractPersistable { public boolean useBorrowerCycle() { return this.useBorrowerCycle; } + + public boolean isMultiDisburseLoan(){ + return this.loanProducTrancheDetails.isMultiDisburseLoan(); + } + + public BigDecimal outstandingLoanBalance() { + return this.loanProducTrancheDetails.outstandingLoanBalance(); + } + + public Integer maxTrancheCount() { + return this.loanProducTrancheDetails.maxTrancheCount(); + } public LoanProductBorrowerCycleVariations fetchLoanProductBorrowerCycleVariationById(Long id) { LoanProductBorrowerCycleVariations borrowerCycleVariation = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index b87e60f5e..0df266ca9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -38,7 +38,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "principal_amount", scale = 6, precision = 19, nullable = false) private BigDecimal principal; - + @Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false) private BigDecimal nominalInterestRatePerPeriod; @@ -155,6 +155,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche return Money.of(this.currency, this.principal); } + public void setPrincipal(BigDecimal principal){ + this.principal = principal; + } public Money getInArrearsTolerance() { return Money.of(this.currency, this.inArrearsTolerance); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductTrancheDetails.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductTrancheDetails.java new file mode 100644 index 000000000..910175fdd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductTrancheDetails.java @@ -0,0 +1,77 @@ +package org.mifosplatform.portfolio.loanproduct.domain; + +import java.math.BigDecimal; +import java.util.Map; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; + +@Embeddable +public class LoanProductTrancheDetails { + + @Column(name = "multi_disburse_loan") + private boolean multiDisburseLoan; + + @Column(name = "max_tranche_count", nullable = false) + private Integer maxTrancheCount; + + @Column(name = "outstanding_loan_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal outstandingLoanBalance; + + protected LoanProductTrancheDetails() { + // TODO Auto-generated constructor stub + } + + public LoanProductTrancheDetails(final boolean multiDisburseLoan, final Integer maxTrancheCount, final BigDecimal outstandingLoanBalance) { + this.multiDisburseLoan = multiDisburseLoan; + this.maxTrancheCount = maxTrancheCount; + this.outstandingLoanBalance = outstandingLoanBalance; + } + + public void update(final JsonCommand command, final Map actualChanges, final String localeAsInput) { + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.multiDisburseLoanParameterName, this.multiDisburseLoan)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.multiDisburseLoanParameterName); + actualChanges.put(LoanProductConstants.multiDisburseLoanParameterName, newValue); + this.multiDisburseLoan = newValue; + } + + if (this.multiDisburseLoan) { + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.maxTrancheCountParameterName, this.maxTrancheCount)) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.maxTrancheCountParameterName); + actualChanges.put(LoanProductConstants.maxTrancheCountParameterName, newValue); + this.maxTrancheCount = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed(LoanProductConstants.outstandingLoanBalanceParameterName, + this.outstandingLoanBalance)) { + final BigDecimal newValue = command + .bigDecimalValueOfParameterNamed(LoanProductConstants.outstandingLoanBalanceParameterName); + actualChanges.put(LoanProductConstants.outstandingLoanBalanceParameterName, newValue); + actualChanges.put(LoanProductConstants.outstandingLoanBalanceParameterName, localeAsInput); + this.outstandingLoanBalance = newValue; + } + } else { + this.maxTrancheCount = null; + this.outstandingLoanBalance = null; + } + } + + + public boolean isMultiDisburseLoan() { + return this.multiDisburseLoan; + } + + + public BigDecimal outstandingLoanBalance() { + return this.outstandingLoanBalance; + } + + + public Integer maxTrancheCount() { + return this.maxTrancheCount; + } + +} 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 ea6a9a573..91597ddce 100644 --- 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 @@ -24,6 +24,7 @@ import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.portfolio.loanproduct.LoanProductConstants; +import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductValueConditionType; import org.springframework.beans.factory.annotation.Autowired; @@ -56,7 +57,9 @@ public final class LoanProductDataValidator { LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),LoanProductConstants.useBorrowerCycleParameterName, LoanProductConstants.principalVariationsForBorrowerCycleParameterName,LoanProductConstants.interestRateVariationsForBorrowerCycleParameterName, - LoanProductConstants.numberOfRepaymentVariationsForBorrowerCycleParameterName, LoanProductConstants.shortName)); + LoanProductConstants.numberOfRepaymentVariationsForBorrowerCycleParameterName,LoanProductConstants.shortName, + LoanProductConstants.multiDisburseLoanParameterName,LoanProductConstants.outstandingLoanBalanceParameterName, + LoanProductConstants.maxTrancheCountParameterName)); private final FromJsonHelper fromApiJsonHelper; @@ -325,11 +328,36 @@ public final class LoanProductDataValidator { validateBorrowerCycleVariations(element, baseDataValidator); } } - + + validateMultiDisburseLoanData(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); } + private void validateMultiDisburseLoanData(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + Boolean multiDisburseLoan = false; + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.multiDisburseLoanParameterName, element)) { + multiDisburseLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.multiDisburseLoanParameterName, element); + baseDataValidator.reset().parameter(LoanProductConstants.multiDisburseLoanParameterName).value(multiDisburseLoan).ignoreIfNull() + .validateForBooleanValue(); + } + + if(multiDisburseLoan){ + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.outstandingLoanBalanceParameterName, element)) { + final BigDecimal outstandingLoanBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanProductConstants.outstandingLoanBalanceParameterName, + element); + baseDataValidator.reset().parameter(LoanProductConstants.outstandingLoanBalanceParameterName).value(outstandingLoanBalance).ignoreIfNull() + .zeroOrPositiveAmount(); + } + + final Integer maxTrancheCount = this.fromApiJsonHelper.extractIntegerNamed(LoanProductConstants.maxTrancheCountParameterName, element,Locale.getDefault()); + baseDataValidator.reset().parameter(LoanProductConstants.maxTrancheCountParameterName).value(maxTrancheCount).notNull().integerGreaterThanZero(); + + final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed("interestType", element, Locale.getDefault()); + baseDataValidator.reset().parameter("interestType").value(interestType).ignoreIfNull().integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue()); + } + } + public void validateForUpdate(final String json, final LoanProduct loanProduct) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -580,6 +608,8 @@ public final class LoanProductDataValidator { validateBorrowerCycleVariations(element, baseDataValidator); } } + + validateMultiDisburseLoanData(baseDataValidator, element); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java index e40908ad4..7f6ceb73e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanEnumerations.java @@ -11,6 +11,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; @@ -416,5 +417,25 @@ public class LoanEnumerations { } return optionData; } + + public static EnumOptionData loanvariationType(final int id) { + return loanvariationType(LoanTermVariationType.fromInt(id)); + } + + public static EnumOptionData loanvariationType(final LoanTermVariationType type) { + EnumOptionData optionData = null; + switch (type) { + case EMI_AMOUNT: + optionData = new EnumOptionData(LoanTermVariationType.EMI_AMOUNT.getValue().longValue(), LoanTermVariationType.EMI_AMOUNT.getCode(), "emiAmount"); + break; + default: + optionData = new EnumOptionData(LoanTermVariationType.INVALID.getValue().longValue(), LoanTermVariationType.INVALID.getCode(), + "Invalid"); + break; + } + return optionData; + } + + } \ No newline at end of file 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 3ec6715a5..33c107210 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.grace_on_principal_periods as graceOnPrincipalPayment, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged," + "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.multi_disburse_loan as multiDisburseLoan, lp.max_tranche_count as maxTrancheCount, lp.outstanding_loan_balance as outstandingLoanBalance, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol, lp.external_id as externalId " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id" @@ -213,6 +214,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo } } } + + + final Boolean multiDisburseLoan = rs.getBoolean("multiDisburseLoan"); + final Integer maxTrancheCount = rs.getInt("maxTrancheCount"); + final BigDecimal outstandingLoanBalance = rs.getBigDecimal("outstandingLoanBalance"); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -221,7 +227,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo transactionStrategyId, transactionStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, this.charges, accountingRuleType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, - numberOfRepaymentVariationsForBorrowerCycle); + numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance); } }