diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java index fa129e094..3cff30b00 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java @@ -37,7 +37,6 @@ import org.mifosng.platform.api.data.LoanChargeData; import org.mifosng.platform.api.data.LoanPermissionData; import org.mifosng.platform.api.data.LoanProductLookup; import org.mifosng.platform.api.data.LoanRepaymentTransactionData; -import org.mifosng.platform.api.data.LoanSchedule; import org.mifosng.platform.api.data.LoanScheduleData; import org.mifosng.platform.api.data.LoanTransactionData; import org.mifosng.platform.api.data.MoneyData; @@ -273,49 +272,50 @@ public class LoansApiResource { if (is(commandParam, "calculateLoanSchedule")) { CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand(); - NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(calculateLoanScheduleCommand); - - final Set typicalLoanScheduleResponseParameters = new HashSet( - Arrays.asList("periods", "cumulativePrincipalDisbursed")); - - Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); - if (responseParameters.isEmpty()) { - responseParameters.addAll(typicalLoanScheduleResponseParameters); - } - boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); - - return this.apiJsonSerializerService.serializeLoanScheduleDataToJson(prettyPrint, responseParameters, loanSchedule); + return calculateLoanSchedule(uriInfo, calculateLoanScheduleCommand); } final EntityIdentifier identifier = this.loanWritePlatformService.submitLoanApplication(command); return this.apiJsonSerializerService.serializeEntityIdentifier(identifier); } + + private String calculateLoanSchedule(final UriInfo uriInfo, final CalculateLoanScheduleCommand command) { + + final NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(command); + + final Set typicalLoanScheduleResponseParameters = new HashSet( + Arrays.asList("periods", "cumulativePrincipalDisbursed")); + + Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); + if (responseParameters.isEmpty()) { + responseParameters.addAll(typicalLoanScheduleResponseParameters); + } + boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); + + return this.apiJsonSerializerService.serializeLoanScheduleDataToJson(prettyPrint, responseParameters, loanSchedule); + } @PUT @Path("{loanId}") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) - public Response modifyLoanApplication( + public String modifyLoanApplication( @PathParam("loanId") final Long loanId, - @QueryParam("command") final String commandParam, +// @QueryParam("command") final String commandParam, +// @Context final UriInfo uriInfo, final String jsonRequestBody) { final LoanApplicationCommand command = this.apiDataConversionService.convertJsonToLoanApplicationCommand(loanId, jsonRequestBody); - // for now just auto generating the loan schedule and setting support - // for 'manual' loan schedule creation later. -// command.setLoanSchedule(loanSchedule); +// if (is(commandParam, "calculateLoanSchedule")) { +// CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand(); +// return calculateLoanSchedule(uriInfo, calculateLoanScheduleCommand); +// } - if (is(commandParam, "calculateLoanSchedule")) { - final CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand(); - final LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand); - return Response.ok().entity(loanSchedule).build(); - } + final EntityIdentifier identifier = this.loanWritePlatformService.modifyLoanApplication(command); - EntityIdentifier identifier = this.loanWritePlatformService.modifyLoanApplication(command); - - return Response.ok().entity(identifier).build(); + return this.apiJsonSerializerService.serializeEntityIdentifier(identifier); } @DELETE @@ -324,8 +324,7 @@ public class LoansApiResource { @Produces({ MediaType.APPLICATION_JSON }) public Response deleteLoanApplication(@PathParam("loanId") final Long loanId) { - EntityIdentifier identifier = this.loanWritePlatformService - .deleteLoan(loanId); + EntityIdentifier identifier = this.loanWritePlatformService.deleteLoan(loanId); return Response.ok().entity(identifier).build(); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/NewLoanScheduleData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/NewLoanScheduleData.java index 0dcc008c5..dd090db1b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/NewLoanScheduleData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/NewLoanScheduleData.java @@ -3,6 +3,7 @@ package org.mifosng.platform.api; import java.math.BigDecimal; import java.util.Collection; +import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedulePeriodData; /** @@ -16,10 +17,18 @@ import org.mifosng.platform.api.data.LoanSchedulePeriodData; @SuppressWarnings("unused") public class NewLoanScheduleData { + private final Integer loanTermInDays; + + /** + * The currency associated with all monetary values in loan schedule. + */ + private final CurrencyData currency; + /** * The cumulative total of all money (principal) disbursed to the loan applicant. */ private final BigDecimal cumulativePrincipalDisbursed; + private final BigDecimal cumulativePrincipalDue; private final BigDecimal cumulativePrincipalPaid; private final BigDecimal cumulativePrincipalOutstanding; @@ -55,13 +64,16 @@ public class NewLoanScheduleData { private final BigDecimal totalInArrears; /** - * periods + * periods is collection of data objects containing specific + * information to each period of the loan schedule including disbursment and + * repayment information. */ private final Collection periods; public NewLoanScheduleData() { this.cumulativePrincipalDisbursed = null; this.cumulativePrincipalPaid = null; + this.cumulativePrincipalDue = null; this.cumulativePrincipalOutstanding = null; this.cumulativeInterestExpected = null; @@ -78,28 +90,52 @@ public class NewLoanScheduleData { this.totalExpectedRepayment = null; this.totalInArrears = null; + this.currency = null; this.periods = null; + this.loanTermInDays = null; } - public NewLoanScheduleData(final Collection periods) { + public NewLoanScheduleData( + final CurrencyData currency, + final Collection periods, + final Integer loanTermInDays, + final BigDecimal cumulativePrincipalDisbursed, + final BigDecimal cumulativePrincipalDue, + final BigDecimal cumulativePrincipalOutstanding, + final BigDecimal cumulativeInterestExpected, + final BigDecimal cumulativeChargesToDate, + final BigDecimal totalExpectedRepayment) { + this.currency = currency; this.periods = periods; + this.loanTermInDays = loanTermInDays; - this.cumulativePrincipalDisbursed = null; + this.cumulativePrincipalDisbursed = cumulativePrincipalDisbursed; + this.cumulativePrincipalDue = cumulativePrincipalDue; this.cumulativePrincipalPaid = null; - this.cumulativePrincipalOutstanding = null; + this.cumulativePrincipalOutstanding = cumulativePrincipalOutstanding; - this.cumulativeInterestExpected = null; + this.cumulativeInterestExpected = cumulativeInterestExpected; this.cumulativeInterestPaid = null; this.cumulativeInterestWaived = null; this.cumulativeInterestOutstanding = null; - this.cumulativeChargesToDate = null; + this.cumulativeChargesToDate = cumulativeChargesToDate; this.cumulativeChargesPaid = null; this.cumulativeChargesOutstanding = null; - this.totalCostOfLoan = null; + if (cumulativeChargesToDate != null) { + this.totalCostOfLoan = cumulativeInterestExpected.add(cumulativeChargesToDate); + } else if (cumulativeInterestExpected != null) { + this.totalCostOfLoan = cumulativeInterestExpected; + } else { + this.totalCostOfLoan = null; + } - this.totalExpectedRepayment = null; + this.totalExpectedRepayment = totalExpectedRepayment; this.totalInArrears = null; } + + public Collection getPeriods() { + return periods; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedule.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedule.java index 9a4bbb0e8..1cd4fc557 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedule.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedule.java @@ -3,6 +3,7 @@ package org.mifosng.platform.api.data; import java.util.ArrayList; import java.util.List; +@Deprecated public class LoanSchedule { private List scheduledLoanInstallments = new ArrayList(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedulePeriodData.java index 13b288c9a..29147259f 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanSchedulePeriodData.java @@ -111,4 +111,28 @@ public class LoanSchedulePeriodData { this.totalDueForPeriod = totalDueForPeriod; } + + public boolean isRepaymentPeriod() { + boolean isRepaymentPeriod = false; + if (principalDue != null && this.interestDue != null) { + isRepaymentPeriod = BigDecimal.ZERO.compareTo(this.principalDue) == -1 || BigDecimal.ZERO.compareTo(this.interestDue) == -1; + } + return isRepaymentPeriod; + } + + public Integer periodNumber() { + return this.period; + } + + public LocalDate periodDueDate() { + return this.dueDate; + } + + public BigDecimal principalDue() { + return this.principalDue; + } + + public BigDecimal interestDue() { + return this.interestDue; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/ScheduledLoanInstallment.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/ScheduledLoanInstallment.java index ab4e54396..1446b866a 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/ScheduledLoanInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/ScheduledLoanInstallment.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import org.joda.time.LocalDate; +@Deprecated public class ScheduledLoanInstallment { private Integer installmentNumber; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java index afbccc1bc..1c5bf5555 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java @@ -439,56 +439,54 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { throw new InvalidJsonException(); } - Type typeOfMap = new TypeToken>(){}.getType(); - Map requestMap = gsonConverter.fromJson(json, typeOfMap); + Type typeOfMap = new TypeToken>(){}.getType(); + Map requestMap = gsonConverter.fromJson(json, typeOfMap); Set supportedParams = new HashSet( - Arrays.asList("clientId", "productId", "externalId", "fundId", "transactionProcessingStrategyId", - "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery", "numberOfRepayments", - "loanTermFrequency", "loanTermFrequencyType", "charges", - "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", - "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", - "locale", "dateFormat", "loanOfficerId") + Arrays.asList("clientId", "productId", "externalId", "fundId", "transactionProcessingStrategyId", + "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery", "numberOfRepayments", + "loanTermFrequency", "loanTermFrequencyType", "charges", + "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", + "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", + "locale", "dateFormat", "loanOfficerId", "id") ); checkForUnsupportedParameters(requestMap, supportedParams); Set modifiedParameters = new HashSet(); - - Long clientId = extractLongParameter("clientId", requestMap, modifiedParameters); - Long productId = extractLongParameter("productId", requestMap, modifiedParameters); - Long fundId = extractLongParameter("fundId", requestMap, modifiedParameters); - Long loanOfficerId = extractLongParameter("loanOfficerId", requestMap, modifiedParameters); - Long transactionProcessingStrategyId = extractLongParameter("transactionProcessingStrategyId", requestMap, modifiedParameters); - String externalId = extractStringParameter("externalId", requestMap, modifiedParameters); - BigDecimal principal = extractBigDecimalParameter("principal", requestMap, modifiedParameters); - BigDecimal inArrearsToleranceValue = extractBigDecimalParameter("inArrearsTolerance", requestMap, modifiedParameters); - BigDecimal interestRatePerPeriod = extractBigDecimalParameter("interestRatePerPeriod", requestMap, modifiedParameters); + JsonParser parser = new JsonParser(); + JsonElement element = parser.parse(json); + JsonParserHelper helper = new JsonParserHelper(); - Integer repaymentEvery = extractIntegerParameter("repaymentEvery", requestMap, modifiedParameters); - Integer numberOfRepayments = extractIntegerParameter("numberOfRepayments", requestMap, modifiedParameters); - Integer repaymentFrequencyType = extractIntegerParameter("repaymentFrequencyType", requestMap, modifiedParameters); + final Long clientId = helper.extractLongNamed("clientId", element, modifiedParameters); + final Long productId = helper.extractLongNamed("productId", element, modifiedParameters); + final Long fundId = helper.extractLongNamed("fundId", element, modifiedParameters); + final Long loanOfficerId = helper.extractLongNamed("loanOfficerId", element, modifiedParameters); + final Long transactionProcessingStrategyId = helper.extractLongNamed("transactionProcessingStrategyId", element, modifiedParameters); + final String externalId = helper.extractStringNamed("externalId", element, modifiedParameters); + final BigDecimal principal = helper.extractBigDecimalNamed("principal", element, modifiedParameters); + final BigDecimal inArrearsToleranceValue = helper.extractBigDecimalNamed("inArrearsTolerance", element, modifiedParameters); + final BigDecimal interestRatePerPeriod = helper.extractBigDecimalNamed("interestRatePerPeriod", element, modifiedParameters); - Integer loanTermFrequency = extractIntegerParameter("loanTermFrequency", requestMap, modifiedParameters); - Integer loanTermFrequencyType = extractIntegerParameter("loanTermFrequencyType", requestMap, modifiedParameters); + final Integer repaymentEvery = helper.extractIntegerNamed("repaymentEvery", element, modifiedParameters); + final Integer numberOfRepayments = helper.extractIntegerNamed("numberOfRepayments", element, modifiedParameters); + final Integer repaymentFrequencyType = helper.extractIntegerNamed("repaymentFrequencyType", element, modifiedParameters); + final Integer loanTermFrequency = helper.extractIntegerNamed("loanTermFrequency", element, modifiedParameters); + final Integer loanTermFrequencyType = helper.extractIntegerNamed("loanTermFrequencyType", element, modifiedParameters); + final Integer interestRateFrequencyType = helper.extractIntegerNamed("interestRateFrequencyType", element, modifiedParameters); + final Integer amortizationType = helper.extractIntegerNamed("amortizationType", element, modifiedParameters); + final Integer interestType = helper.extractIntegerNamed("interestType", element, modifiedParameters); + final Integer interestCalculationPeriodType = helper.extractIntegerNamed("interestCalculationPeriodType", element, modifiedParameters); - Integer interestRateFrequencyTypeValue = extractIntegerParameter("interestRateFrequencyType", requestMap, modifiedParameters); - Integer amortizationTypeValue = extractIntegerParameter("amortizationType", requestMap, modifiedParameters); - Integer interestTypeValue = extractIntegerParameter("interestType", requestMap, modifiedParameters); - Integer interestCalculationPeriodTypeValue = extractIntegerParameter("interestCalculationPeriodType", requestMap, modifiedParameters); + final LocalDate expectedDisbursementDate = helper.extractLocalDateNamed("expectedDisbursementDate", element, modifiedParameters); + final LocalDate repaymentsStartingFromDate = helper.extractLocalDateNamed("repaymentsStartingFromDate", element, modifiedParameters); + final LocalDate interestChargedFromDate = helper.extractLocalDateNamed("interestChargedFromDate", element, modifiedParameters); + final LocalDate submittedOnDate = helper.extractLocalDateNamed("submittedOnDate", element, modifiedParameters); - LocalDate expectedDisbursementDate = extractLocalDateParameter("expectedDisbursementDate", requestMap, modifiedParameters); - LocalDate repaymentsStartingFromDate = extractLocalDateParameter("repaymentsStartingFromDate", requestMap, modifiedParameters); - LocalDate interestChargedFromDate = extractLocalDateParameter("interestChargedFromDate", requestMap, modifiedParameters); - LocalDate submittedOnDate = extractLocalDateParameter("submittedOnDate", requestMap, modifiedParameters); + final String submittedOnNote = helper.extractStringNamed("submittedOnNote", element, modifiedParameters); - String submittedOnNote = extractStringParameter("submittedOnNote", requestMap, modifiedParameters); - - JsonParser parser = new JsonParser(); - LoanChargeCommand[] charges = null; - JsonElement element = parser.parse(json); if (element.isJsonObject()) { JsonObject object = element.getAsJsonObject(); if (object.has("charges") && object.get("charges").isJsonArray()) { @@ -518,8 +516,8 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { resourceIdentifier, clientId, productId, externalId, fundId, transactionProcessingStrategyId, submittedOnDate, submittedOnNote, expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate, - principal, interestRatePerPeriod, interestRateFrequencyTypeValue, interestTypeValue, interestCalculationPeriodTypeValue, - repaymentEvery, repaymentFrequencyType, numberOfRepayments, amortizationTypeValue, + principal, interestRatePerPeriod, interestRateFrequencyType, interestType, interestCalculationPeriodType, + repaymentEvery, repaymentFrequencyType, numberOfRepayments, amortizationType, loanTermFrequency, loanTermFrequencyType, inArrearsToleranceValue, charges,loanOfficerId); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/JsonParserHelper.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/JsonParserHelper.java new file mode 100644 index 000000000..b597b206c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/JsonParserHelper.java @@ -0,0 +1,353 @@ +package org.mifosng.platform.api.infrastructure; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +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.joda.time.format.DateTimeFormat; +import org.mifosng.platform.api.data.ApiParameterError; +import org.mifosng.platform.exceptions.PlatformApiDataValidationException; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.format.number.NumberFormatter; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * Helper class to extract values of json named attributes. + */ +public class JsonParserHelper { + + public Long extractLongNamed(final String parameterName, final JsonElement element, final Set modifiedParameters) { + Long longValue = null; + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) { + modifiedParameters.add(parameterName); + JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive(); + final String stringValue = primitive.getAsString(); + if (StringUtils.isNotBlank(stringValue)) { + longValue = Long.valueOf(stringValue); + } + } + } + return longValue; + } + + public String extractStringNamed(final String parameterName, final JsonElement element, final Set modifiedParameters) { + String stringValue = null; + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) { + modifiedParameters.add(parameterName); + JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive(); + if (StringUtils.isNotBlank(stringValue)) { + stringValue = primitive.getAsString(); + } + } + } + return stringValue; + } + + public BigDecimal extractBigDecimalNamed(final String parameterName, final JsonElement element, final Set modifiedParameters) { + BigDecimal value = null; + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) { + modifiedParameters.add(parameterName); + JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive(); + String valueAsString = primitive.getAsString(); + if (StringUtils.isNotBlank(valueAsString)) { + value = convertFrom(valueAsString, parameterName, extractLocaleValue(object)); + } + } + } + return value; + } + + public Integer extractIntegerNamed(final String parameterName, final JsonElement element, final Set modifiedParameters) { + Integer value = null; + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) { + modifiedParameters.add(parameterName); + JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive(); + String valueAsString = primitive.getAsString(); + if (StringUtils.isNotBlank(valueAsString)) { + value = convertToInteger(valueAsString, parameterName, extractLocaleValue(object)); + } + } + } + return value; + } + + public LocalDate extractLocalDateNamed(final String parameterName, final JsonElement element, final Set modifiedParameters) { + LocalDate value = null; + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + + String dateFormat = null; + final String dateFormatParameter = "dateFormat"; + if (object.has(dateFormatParameter) && object.get(dateFormatParameter).isJsonPrimitive()) { + JsonPrimitive primitive = object.get(dateFormatParameter).getAsJsonPrimitive(); + dateFormat = primitive.getAsString(); + } + + if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) { + modifiedParameters.add(parameterName); + JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive(); + final String valueAsString = primitive.getAsString(); + if (StringUtils.isNotBlank(valueAsString)) { + value = convertFrom(valueAsString, parameterName, dateFormat); + } + } + + } + return value; + } + + private LocalDate convertFrom(final String dateAsString, final String parameterName, final String dateFormat) { + + if (StringUtils.isBlank(dateFormat)) { + + List dataValidationErrors = new ArrayList(); + String defaultMessage = new StringBuilder("The parameter '" + parameterName + "' requires a 'dateFormat' parameter to be passed with it.").toString(); + ApiParameterError error = ApiParameterError.parameterError("validation.msg.missing.dateFormat.parameter", defaultMessage, parameterName); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + + LocalDate eventLocalDate = null; + if (StringUtils.isNotBlank(dateAsString)) { + try { + Locale locale = LocaleContextHolder.getLocale(); + eventLocalDate = DateTimeFormat.forPattern(dateFormat) + .withLocale(locale) + .parseLocalDate(dateAsString.toLowerCase(locale)); + } catch (IllegalArgumentException e) { + List dataValidationErrors = new ArrayList(); + ApiParameterError error = ApiParameterError + .parameterError( + "validation.msg.invalid.date.format", + "The parameter " + + parameterName + + " is invalid based on the dateFormat provided:" + + dateFormat, parameterName, + dateAsString, dateFormat); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + } + + return eventLocalDate; + } + + private BigDecimal convertFrom(final String numericalValueFormatted, final String parameterName, final Locale clientApplicationLocale) { + + if (clientApplicationLocale == null) { + + List dataValidationErrors = new ArrayList(); + String defaultMessage = new StringBuilder("The parameter '" + parameterName + "' requires a 'locale' parameter to be passed with it.").toString(); + ApiParameterError error = ApiParameterError.parameterError("validation.msg.missing.locale.parameter", defaultMessage, parameterName); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + + try { + BigDecimal number = null; + + if (StringUtils.isNotBlank(numericalValueFormatted)) { + + String source = numericalValueFormatted.trim(); + + NumberFormat format = NumberFormat.getNumberInstance(clientApplicationLocale); + DecimalFormat df = (DecimalFormat) format; + DecimalFormatSymbols symbols = df.getDecimalFormatSymbols(); + // http://bugs.sun.com/view_bug.do?bug_id=4510618 + char groupingSeparator = symbols.getGroupingSeparator(); + if (groupingSeparator == '\u00a0') { + source = source.replaceAll(" ", Character.toString('\u00a0')); + } + + NumberFormatter numberFormatter = new NumberFormatter(); + Number parsedNumber = numberFormatter.parse(source, clientApplicationLocale); + number = BigDecimal.valueOf(Double.valueOf(parsedNumber.doubleValue())); + } + + return number; + } catch (ParseException e) { + + List dataValidationErrors = new ArrayList(); + ApiParameterError error = ApiParameterError.parameterError( + "validation.msg.invalid.decimal.format", "The parameter " + + parameterName + " has value: " + numericalValueFormatted + " which is invalid decimal value for provided locale of [" + + clientApplicationLocale.toString() + "].", + parameterName, numericalValueFormatted, + clientApplicationLocale); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + } + +private Integer convertToInteger(final String numericalValueFormatted, final String parameterName, final Locale clientApplicationLocale) { + + if (clientApplicationLocale == null) { + + List dataValidationErrors = new ArrayList(); + String defaultMessage = new StringBuilder("The parameter '" + parameterName + "' requires a 'locale' parameter to be passed with it.").toString(); + ApiParameterError error = ApiParameterError.parameterError("validation.msg.missing.locale.parameter", defaultMessage, parameterName); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + + try { + Integer number = null; + + if (StringUtils.isNotBlank(numericalValueFormatted)) { + + String source = numericalValueFormatted.trim(); + + NumberFormat format = NumberFormat.getInstance(clientApplicationLocale); + DecimalFormat df = (DecimalFormat) format; + DecimalFormatSymbols symbols = df.getDecimalFormatSymbols(); + df.setParseBigDecimal(true); + + // http://bugs.sun.com/view_bug.do?bug_id=4510618 + char groupingSeparator = symbols.getGroupingSeparator(); + if (groupingSeparator == '\u00a0') { + source = source.replaceAll(" ", Character.toString('\u00a0')); + } + + Number parsedNumber = df.parse(source); + + double parsedNumberDouble = parsedNumber.doubleValue(); + int parsedNumberInteger = parsedNumber.intValue(); + + if (source.contains(Character.toString(symbols.getDecimalSeparator()))) { + throw new ParseException(source, 0); + } + + if (!Double.valueOf(parsedNumberDouble).equals(Double.valueOf(Integer.valueOf(parsedNumberInteger)))) { + throw new ParseException(source, 0); + } + + number = parsedNumber.intValue(); + } + + return number; + } catch (ParseException e) { + + List dataValidationErrors = new ArrayList(); + ApiParameterError error = ApiParameterError.parameterError( + "validation.msg.invalid.integer.format", "The parameter " + + parameterName + " has value: " + numericalValueFormatted + " which is invalid integer value for provided locale of [" + + clientApplicationLocale.toString() + "].", + parameterName, numericalValueFormatted, + clientApplicationLocale); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + } + + private Locale localeFromString(final String localeAsString) { + + if (StringUtils.isBlank(localeAsString)) { + List dataValidationErrors = new ArrayList(); + ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.locale.format", "The parameter locale is invalid. It cannot be blank.", "locale"); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + + String languageCode = ""; + String courntryCode = ""; + String variantCode = ""; + + String[] localeParts = localeAsString.split("_"); + + if (localeParts != null && localeParts.length == 1) { + languageCode = localeParts[0]; + } + + if (localeParts != null && localeParts.length == 2) { + languageCode = localeParts[0]; + courntryCode = localeParts[1]; + } + + if (localeParts != null && localeParts.length == 3) { + languageCode = localeParts[0]; + courntryCode = localeParts[1]; + variantCode = localeParts[2]; + } + + return localeFrom(languageCode, courntryCode, variantCode); + } + + private Locale localeFrom(final String languageCode, final String courntryCode, final String variantCode) { + + List dataValidationErrors = new ArrayList(); + + List allowedLanguages = Arrays.asList(Locale.getISOLanguages()); + if (!allowedLanguages.contains(languageCode.toLowerCase())) { + ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.locale.format", "The parameter locale has an invalid language value " + languageCode + " .", "locale", languageCode); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(courntryCode.toUpperCase())) { + List allowedCountries = Arrays.asList(Locale.getISOCountries()); + if (!allowedCountries.contains(courntryCode)) { + ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.locale.format", "The parameter locale has an invalid country value " + courntryCode + " .", "locale", courntryCode); + dataValidationErrors.add(error); + } + } + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException( + "validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); + } + + return new Locale(languageCode.toLowerCase(), courntryCode.toUpperCase(), variantCode); + } + + private Locale extractLocaleValue(final JsonObject object) { + Locale clientApplicationLocale = null; + String locale = null; + if (object.has("locale") && object.get("locale").isJsonPrimitive()) { + JsonPrimitive primitive = object.get("locale").getAsJsonPrimitive(); + locale = primitive.getAsString(); + clientApplicationLocale = localeFromString(locale); + } + return clientApplicationLocale; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 4e3a95835..c5e34e987 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -27,10 +27,9 @@ import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.DateTime; import org.joda.time.LocalDate; +import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.commands.LoanApplicationCommand; -import org.mifosng.platform.api.data.LoanSchedule; -import org.mifosng.platform.api.data.MoneyData; -import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.client.domain.Client; import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; @@ -244,7 +243,7 @@ public class Loan extends AbstractAuditableCustom { } public void modifyLoanApplication(final LoanApplicationCommand command, final Client client, final LoanProduct loanProduct, - final Fund fund, final LoanTransactionProcessingStrategy strategy, final LoanSchedule modifiedLoanSchedule, final Set charges, + final Fund fund, final LoanTransactionProcessingStrategy strategy, final NewLoanScheduleData modifiedLoanSchedule, final Set charges, final Staff loanOfficer) { if (command.isClientChanged()) { @@ -310,16 +309,16 @@ public class Loan extends AbstractAuditableCustom { // FIXME - rewrite over loan schedule by default for now but worth putting in check to see if required // i.e. only a client change wouldn't require it, only if one of parameters related to loan schedule calculation is changed. this.repaymentScheduleInstallments.clear(); - for (ScheduledLoanInstallment scheduledLoanInstallment : modifiedLoanSchedule.getScheduledLoanInstallments()) { - - MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue(); - MoneyData readInterestDue = scheduledLoanInstallment.getInterestDue(); - - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( - this, scheduledLoanInstallment.getInstallmentNumber(), - scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(), - readInterestDue.getAmount()); - this.addRepaymentScheduleInstallment(installment); + for (LoanSchedulePeriodData scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { + + if (scheduledLoanInstallment.isRepaymentPeriod()) { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + this, scheduledLoanInstallment.periodNumber(), + scheduledLoanInstallment.periodDueDate(), + scheduledLoanInstallment.principalDue(), + scheduledLoanInstallment.interestDue()); + this.addRepaymentScheduleInstallment(installment); + } } // if the loan application/contract is modified when repayments are already against it - then need to re-process it diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformService.java index 569466174..b5f64fbb9 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformService.java @@ -3,12 +3,9 @@ package org.mifosng.platform.loan.service; import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.commands.CalculateLoanScheduleCommand; -import org.mifosng.platform.api.data.LoanSchedule; public interface CalculationPlatformService { - LoanSchedule calculateLoanSchedule(CalculateLoanScheduleCommand command); - NewLoanScheduleData calculateLoanScheduleNew(CalculateLoanScheduleCommand command); /** diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java index 5202b6f06..b53154d88 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/CalculationPlatformServiceImpl.java @@ -1,30 +1,20 @@ package org.mifosng.platform.loan.service; -import java.math.BigDecimal; - import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.commands.CalculateLoanScheduleCommand; import org.mifosng.platform.api.data.CurrencyData; -import org.mifosng.platform.api.data.LoanSchedule; import org.mifosng.platform.api.data.MoneyData; import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.currency.domain.ApplicationCurrencyRepository; -import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.exceptions.CurrencyNotFoundException; import org.mifosng.platform.exceptions.LoanNotFoundException; -import org.mifosng.platform.exceptions.LoanProductNotFoundException; -import org.mifosng.platform.loan.domain.AmortizationMethod; -import org.mifosng.platform.loan.domain.InterestCalculationPeriodMethod; import org.mifosng.platform.loan.domain.InterestMethod; import org.mifosng.platform.loan.domain.Loan; import org.mifosng.platform.loan.domain.LoanPayoffSummary; -import org.mifosng.platform.loan.domain.LoanProduct; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; -import org.mifosng.platform.loan.domain.LoanProductRepository; import org.mifosng.platform.loan.domain.LoanRepository; import org.mifosng.platform.loan.domain.PeriodFrequencyType; -import org.mifosng.platform.loanschedule.domain.AprCalculator; import org.mifosng.platform.loanschedule.domain.DefaultLoanScheduleGeneratorFactory; import org.mifosng.platform.loanschedule.domain.LoanScheduleGenerator; import org.mifosng.platform.loanschedule.domain.LoanScheduleGeneratorFactory; @@ -37,9 +27,7 @@ public class CalculationPlatformServiceImpl implements CalculationPlatformServic private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanRepository loanRepository; - private final LoanProductRepository loanProductRepository; private final ApplicationCurrencyRepository applicationCurrencyRepository; - private final AprCalculator aprCalculator; private final PlatformSecurityContext context; private final LoanProductRelatedDetailAssembler loanProductRelatedDetailAssembler; @@ -47,15 +35,11 @@ public class CalculationPlatformServiceImpl implements CalculationPlatformServic public CalculationPlatformServiceImpl( final PlatformSecurityContext context, final LoanRepository loanRepository, - final LoanProductRepository loanProductRepository, final ApplicationCurrencyRepository applicationCurrencyRepository, - final AprCalculator aprCalculator, final LoanProductRelatedDetailAssembler loanProductRelatedDetailAssembler) { this.context = context; this.loanRepository = loanRepository; - this.loanProductRepository = loanProductRepository; this.applicationCurrencyRepository = applicationCurrencyRepository; - this.aprCalculator = aprCalculator; this.loanScheduleFactory = new DefaultLoanScheduleGeneratorFactory(); this.loanProductRelatedDetailAssembler = loanProductRelatedDetailAssembler; } @@ -70,76 +54,19 @@ public class CalculationPlatformServiceImpl implements CalculationPlatformServic LoanProductRelatedDetail loanScheduleRelatedDetails = this.loanProductRelatedDetailAssembler.assembleFrom(command); -// final Integer loanTermFrequency = command.getLoanTermFrequency(); -// final PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType()); - final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType()); - - LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod); - -// ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(loanScheduleRelatedDetails.getCurrency().getCode()); - -// CurrencyData currencyData = new CurrencyData( -// applicationCurrency.getCode(), -// applicationCurrency.getName(), -// loanScheduleRelatedDetails.getCurrency().getDigitsAfterDecimal(), -// applicationCurrency.getDisplaySymbol(), -// applicationCurrency.getNameCode()); - - return loanScheduleGenerator.generate(loanScheduleRelatedDetails, - command.getExpectedDisbursementDate(), - command.getRepaymentsStartingFromDate(), - command.getInterestChargedFromDate()); - } - - @Override - public LoanSchedule calculateLoanSchedule(final CalculateLoanScheduleCommand command) { - - context.authenticatedUser(); - - CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command); - validator.validate(); - - final BigDecimal principalAmount = command.getPrincipal(); - final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriod(); - final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType()); - final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType()); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType()); - final Integer loanTermFrequency = command.getLoanTermFrequency(); final PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.fromInt(command.getLoanTermFrequencyType()); - - final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); - final Integer repayEvery = command.getRepaymentEvery(); - final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType()); - - final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType()); - - final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestPeriodFrequencyType, defaultNominalInterestRatePerPeriod); - - LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId()); - if (loanProduct == null) { - throw new LoanProductNotFoundException(command.getProductId()); - } - - final MonetaryCurrency currency = loanProduct.getCurrency(); - - // in arrerars tolerance isnt relevant when auto-calculating a loan schedule - final BigDecimal inArrearsTolerance = BigDecimal.ZERO; - - LoanProductRelatedDetail loanScheduleInfo = new LoanProductRelatedDetail(currency, principalAmount, - defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, - interestMethod, interestCalculationPeriodMethod, - repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, - inArrearsTolerance); + final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType()); LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod); - ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode()); - CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), currency.getDigitsAfterDecimal(), - applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode()); + ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(loanScheduleRelatedDetails.getCurrency().getCode()); - return loanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), - command.getInterestChargedFromDate(), currencyData); + return loanScheduleGenerator.generate(applicationCurrency, + loanScheduleRelatedDetails, loanTermFrequency, + loanTermFrequencyType, command.getExpectedDisbursementDate(), + command.getRepaymentsStartingFromDate(), + command.getInterestChargedFromDate()); } @Override diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java index 6f9d5a43e..3af2cfd0c 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java @@ -1,16 +1,15 @@ package org.mifosng.platform.loan.service; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.commands.LoanApplicationCommand; import org.mifosng.platform.api.commands.LoanChargeCommand; -import org.mifosng.platform.api.data.MoneyData; -import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.charge.domain.Charge; import org.mifosng.platform.charge.domain.ChargeRepository; import org.mifosng.platform.client.domain.Client; @@ -59,6 +58,7 @@ public class LoanAssembler { private final ChargeRepository chargeRepository; private final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository; private final StaffRepository staffRepository; + private final CalculationPlatformService calculationPlatformService; @Autowired public LoanAssembler( @@ -67,13 +67,15 @@ public class LoanAssembler { final FundRepository fundRepository, final ChargeRepository chargeRepository, final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository, - final StaffRepository staffRepository) { + final StaffRepository staffRepository, + final CalculationPlatformService calculationPlatformService) { this.loanProductRepository = loanProductRepository; this.clientRepository = clientRepository; this.fundRepository = fundRepository; this.chargeRepository = chargeRepository; this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository; this.staffRepository = staffRepository; + this.calculationPlatformService = calculationPlatformService; } public Loan assembleFrom(final LoanApplicationCommand command) { @@ -107,21 +109,17 @@ public class LoanAssembler { Loan loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail, loanCharges); loan.setExternalId(command.getExternalId()); - // TODO - user service to calculate loan schedule at this point -// LoanSchedule loanSchedule = command.getLoanSchedule(); - List loanRepaymentSchedule = new ArrayList(); -// loanSchedule.getScheduledLoanInstallments(); + final NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(command.toCalculateLoanScheduleCommand()); - for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) { - - MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue(); - MoneyData readInterestDue = scheduledLoanInstallment.getInterestDue(); - - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( - loan, scheduledLoanInstallment.getInstallmentNumber(), - scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(), - readInterestDue.getAmount()); - loan.addRepaymentScheduleInstallment(installment); + for (LoanSchedulePeriodData scheduledLoanInstallment : loanSchedule.getPeriods()) { + if (scheduledLoanInstallment.isRepaymentPeriod()) { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + loan, scheduledLoanInstallment.periodNumber(), + scheduledLoanInstallment.periodDueDate(), + scheduledLoanInstallment.principalDue(), + scheduledLoanInstallment.interestDue()); + loan.addRepaymentScheduleInstallment(installment); + } } loan.submitApplication(loanTermFrequency, loanTermFrequencyType, diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 0ac291f82..935ffb6f4 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -8,6 +8,7 @@ import java.util.Set; import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; +import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.commands.AdjustLoanTransactionCommand; import org.mifosng.platform.api.commands.CalculateLoanScheduleCommand; import org.mifosng.platform.api.commands.LoanApplicationCommand; @@ -15,13 +16,11 @@ import org.mifosng.platform.api.commands.LoanStateTransitionCommand; import org.mifosng.platform.api.commands.LoanTransactionCommand; import org.mifosng.platform.api.commands.UndoStateTransitionCommand; import org.mifosng.platform.api.data.EntityIdentifier; -import org.mifosng.platform.api.data.LoanSchedule; -import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.client.domain.Client; import org.mifosng.platform.client.domain.ClientRepository; import org.mifosng.platform.client.domain.Note; import org.mifosng.platform.client.domain.NoteRepository; -import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.exceptions.ClientNotFoundException; import org.mifosng.platform.exceptions.LoanNotFoundException; @@ -147,7 +146,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf LoanTransactionProcessingStrategy strategy = this.loanAssembler.findStrategyByIdIfProvided(command.getTransactionProcessingStrategyId()); Set charges = this.loanAssembler.assembleSetOfLoanCharges(command.getCharges(), loanProduct.getCharges(), loan.getCurrency().getCode()); - LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command.toCalculateLoanScheduleCommand()); + final NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(command.toCalculateLoanScheduleCommand()); loan.modifyLoanApplication(command, client, loanProduct, fund, strategy, loanSchedule, charges, loanOfficer); this.loanRepository.save(loan); @@ -332,7 +331,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Integer selectedRepaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType().getValue(); Integer selectedAmortizationMethod = loan.repaymentScheduleDetail().getAmortizationMethod().getValue(); - // FIXME - use values from loan table here instead of inferring. + // FIXME - KW - use values from loan table here instead of inferring. Integer loanTermFrequency = repaidEvery * numberOfInstallments; Integer loanTermFrequencyType = selectedRepaymentFrequency; @@ -351,29 +350,20 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loanTermFrequency, loanTermFrequencyType, actualDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate); - LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateCommand); + NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(calculateCommand); List modifiedLoanRepaymentSchedule = new ArrayList(); - for (ScheduledLoanInstallment scheduledLoanInstallment : loanSchedule - .getScheduledLoanInstallments()) { - - final MonetaryCurrency monetaryCurrency = new MonetaryCurrency( - scheduledLoanInstallment.getPrincipalDue().getCurrencyCode(), - scheduledLoanInstallment.getPrincipalDue().getDigitsAfterDecimal()); - - Money principal = Money.of(monetaryCurrency, - scheduledLoanInstallment.getPrincipalDue().getAmount()); - - Money interest = Money.of(monetaryCurrency, - scheduledLoanInstallment.getInterestDue().getAmount()); - - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( - loan, scheduledLoanInstallment.getInstallmentNumber(), -// scheduledLoanInstallment.getPeriodStart(), - scheduledLoanInstallment.getPeriodEnd(), principal.getAmount(), - interest.getAmount()); - modifiedLoanRepaymentSchedule.add(installment); + for (LoanSchedulePeriodData scheduledLoanPeriod : loanSchedule.getPeriods()) { + if (scheduledLoanPeriod.isRepaymentPeriod()) { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + loan, scheduledLoanPeriod.periodNumber(), + scheduledLoanPeriod.periodDueDate(), + scheduledLoanPeriod.principalDue(), + scheduledLoanPeriod.interestDue()); + + modifiedLoanRepaymentSchedule.add(installment); + } } loan.disburseWithModifiedRepaymentSchedule(actualDisbursementDate, modifiedLoanRepaymentSchedule, defaultLoanLifecycleStateMachine()); } else { @@ -402,7 +392,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } if (loan.isActualDisbursedOnDateEarlierOrLaterThanExpected()) { - // FIXME - handle this use case - recalculate loan schedule using original settings. + // FIXME - KW - handle this use case - recalculate loan schedule using original settings. } loan.undoDisbursal(defaultLoanLifecycleStateMachine()); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java index 036f6cc31..dda180df9 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/AmortizationLoanScheduleGenerator.java @@ -7,17 +7,23 @@ import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedule; +import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; public interface AmortizationLoanScheduleGenerator { - LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate, - LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, CurrencyData currencyData, - BigDecimal periodInterestRateForRepaymentPeriod, - LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, - List scheduledDates); + @Deprecated + LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo, + final LocalDate disbursementDate, + final LocalDate firstRepaymentDate, + final LocalDate interestCalculatedFrom, + final CurrencyData currencyData, + final BigDecimal periodInterestRateForRepaymentPeriod, + final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, + final List scheduledDates); NewLoanScheduleData generate( + ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate, LocalDate firstRepaymentDate, @@ -25,5 +31,4 @@ public interface AmortizationLoanScheduleGenerator { BigDecimal periodInterestRateForRepaymentPeriod, LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, List scheduledDates); - -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java index 9f5207234..ff5527b79 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java @@ -7,6 +7,7 @@ import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedule; +import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.loan.domain.AmortizationMethod; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; import org.mifosng.platform.loan.domain.PeriodFrequencyType; @@ -36,7 +37,10 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule @Override public NewLoanScheduleData generate( - final LoanProductRelatedDetail loanScheduleInfo, + final ApplicationCurrency applicationCurrency, + final LoanProductRelatedDetail loanScheduleInfo, + final Integer loanTermFrequency, + final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom) { @@ -52,7 +56,7 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule // Determine with 'amortisation' approach to use final AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo.getAmortizationMethod()); - return generator.generate( + return generator.generate(applicationCurrency, loanScheduleInfo, disbursementDate, firstRepaymentDate, @@ -62,6 +66,7 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule scheduledDates); } + @Deprecated @Override public LoanSchedule generate( final LoanProductRelatedDetail loanScheduleInfo, diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java index 85a52a1da..fe4c217e3 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java @@ -13,6 +13,7 @@ import org.mifosng.platform.api.data.LoanSchedule; import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.api.data.MoneyData; import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; @@ -28,6 +29,7 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort @Override public NewLoanScheduleData generate( + final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, final LocalDate disbursementDate, final LocalDate firstRepaymentDate, @@ -36,10 +38,7 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, final List scheduledDates) { - // 1. add disbursements to loan schedule - - // 2. - Collection periods = new ArrayList(); + final Collection periods = new ArrayList(); // determine 'total payment' for each repayment based on pmt function (and hence the total due overall) final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); @@ -58,11 +57,17 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate); - // create entries of disbursement period on loan schedule final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursement(disbursementDate, loanScheduleInfo.getPrincipal().getAmount()); periods.add(disbursementPeriod); + int loanTermInDays = Integer.valueOf(0); + BigDecimal cumulativePrincipalDisbursed = loanScheduleInfo.getPrincipal().getAmount(); + BigDecimal cumulativePrincipalDue = BigDecimal.ZERO; + BigDecimal cumulativeInterestExpected = BigDecimal.ZERO; + BigDecimal cumulativeChargesToDate = BigDecimal.ZERO; + BigDecimal totalExpectedRepayment = BigDecimal.ZERO; + LocalDate startDate = disbursementDate; int periodNumber = 1; for (LocalDate scheduledDueDate : scheduledDates) { @@ -115,15 +120,31 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort interestForInstallment.getAmount(), totalInstallmentDue.getAmount()); periods.add(installment); - + + // handle cumulative fields + loanTermInDays += daysInPeriod; + cumulativePrincipalDue = cumulativePrincipalDue.add(principalForInstallment.getAmount()); + cumulativeInterestExpected = cumulativeInterestExpected.add(interestForInstallment.getAmount()); + totalExpectedRepayment = totalExpectedRepayment.add(totalInstallmentDue.getAmount()); startDate = scheduledDueDate; periodNumber++; } - return new NewLoanScheduleData(periods); + final BigDecimal cumulativePrincipalOutstanding = cumulativePrincipalDisbursed.subtract(cumulativePrincipalDue); + + CurrencyData currencyData = new CurrencyData( + currency.getCode(), + currency.getName(), + monetaryCurrency.getDigitsAfterDecimal(), + currency.getDisplaySymbol(), + currency.getNameCode()); + + return new NewLoanScheduleData(currencyData, periods, loanTermInDays, cumulativePrincipalDisbursed, cumulativePrincipalDue, + cumulativePrincipalOutstanding, cumulativeInterestExpected, cumulativeChargesToDate, totalExpectedRepayment); } + @Deprecated @Override public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo, final LocalDate disbursementDate, diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java index f179ef084..c4e8b433b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java @@ -2,6 +2,7 @@ package org.mifosng.platform.loanschedule.domain; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.joda.time.Days; @@ -9,8 +10,10 @@ import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedule; +import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.api.data.MoneyData; import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; @@ -22,16 +25,106 @@ public class EqualPrincipalAmortizationLoanScheduleGenerator implements Amortiza @Override public NewLoanScheduleData generate( + ApplicationCurrency currency, LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, BigDecimal periodInterestRateForRepaymentPeriod, LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, List scheduledDates) { - // TODO Auto-generated method stub - return null; + + final Collection periods = new ArrayList(); + + // 1. we know 'principal' must be equal over repayments so use this to calculate interest and thus total repayment due. + final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); + Money totalDuePerInstallment = Money.zero(monetaryCurrency); + + Money outstandingBalance = loanScheduleInfo.getPrincipal(); + Money totalPrincipal = Money.zero(monetaryCurrency); + Money totalInterest = Money.zero(monetaryCurrency); + + double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), + loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate); + + // create entries of disbursement period on loan schedule + final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursement(disbursementDate, loanScheduleInfo.getPrincipal().getAmount()); + periods.add(disbursementPeriod); + + int loanTermInDays = Integer.valueOf(0); + BigDecimal cumulativePrincipalDisbursed = loanScheduleInfo.getPrincipal().getAmount(); + BigDecimal cumulativePrincipalDue = BigDecimal.ZERO; + BigDecimal cumulativeInterestExpected = BigDecimal.ZERO; + BigDecimal cumulativeChargesToDate = BigDecimal.ZERO; + BigDecimal totalExpectedRepayment = BigDecimal.ZERO; + + LocalDate startDate = disbursementDate; + int periodNumber = 1; + for (LocalDate scheduledDueDate : scheduledDates) { + + // number of days from startDate to this scheduledDate + int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays(); + + Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo); + Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, interestForInstallment, loanScheduleInfo); + + if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { + Money graceOnInterestForRepaymentPeriod = interestForInstallment; + interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); + interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue(); + } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { + Money graceOnInterestForRepaymentPeriod = interestForInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); + interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); + interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); + } + + totalPrincipal = totalPrincipal.plus(principalForInstallment); + totalInterest = totalInterest.plus(interestForInstallment); + + if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { + Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); + if (principalDifference.isLessThanZero()) { + principalForInstallment = principalForInstallment.plus(principalDifference.abs()); + } else if (principalDifference.isGreaterThanZero()) { + principalForInstallment = principalForInstallment.minus(principalDifference.abs()); + } + } + + Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment); + + outstandingBalance = outstandingBalance.minus(principalForInstallment); + + LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentPeriod(periodNumber, startDate, + scheduledDueDate, + principalForInstallment.getAmount(), + outstandingBalance.getAmount(), + interestForInstallment.getAmount(), totalInstallmentDue.getAmount()); + + periods.add(installment); + + // handle cumulative fields + loanTermInDays += daysInPeriod; + cumulativePrincipalDue = cumulativePrincipalDue.add(principalForInstallment.getAmount()); + cumulativeInterestExpected = cumulativeInterestExpected.add(interestForInstallment.getAmount()); + totalExpectedRepayment = totalExpectedRepayment.add(totalInstallmentDue.getAmount()); + startDate = scheduledDueDate; + + periodNumber++; + } + + final BigDecimal cumulativePrincipalOutstanding = cumulativePrincipalDisbursed.subtract(cumulativePrincipalDue); + + CurrencyData currencyData = new CurrencyData( + currency.getCode(), + currency.getName(), + monetaryCurrency.getDigitsAfterDecimal(), + currency.getDisplaySymbol(), + currency.getNameCode()); + + return new NewLoanScheduleData(currencyData, periods, loanTermInDays, cumulativePrincipalDisbursed, cumulativePrincipalDue, + cumulativePrincipalOutstanding, cumulativeInterestExpected, cumulativeChargesToDate, totalExpectedRepayment); } + @Deprecated @Override public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo, final LocalDate disbursementDate, diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java index 3636ca5af..0b87c4ad4 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatMethodLoanScheduleGenerator.java @@ -4,14 +4,19 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import org.joda.time.Days; import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedule; +import org.mifosng.platform.api.data.LoanSchedulePeriodData; import org.mifosng.platform.api.data.MoneyData; import org.mifosng.platform.api.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.ApplicationCurrency; +import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; import org.mifosng.platform.loan.domain.PeriodFrequencyType; @@ -23,10 +28,108 @@ public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator { @Override public NewLoanScheduleData generate( - LoanProductRelatedDetail loanScheduleInfo, - LocalDate disbursementDate, LocalDate firstRepaymentDate, - LocalDate interestCalculatedFrom) { - return null; + final ApplicationCurrency currency, + final LoanProductRelatedDetail loanScheduleInfo, + final Integer loanTermFrequency, + final PeriodFrequencyType loanTermFrequencyType, + final LocalDate disbursementDate, + final LocalDate firstRepaymentDate, + final LocalDate interestCalculatedFrom) { + + final Collection periods = new ArrayList(); + + final List scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate); + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType)); + BigDecimal interestRateForLoanTerm = loanScheduleInfo + .getAnnualNominalInterestRate() + .divide(loanTermPeriodsInYear, mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanTermFrequency)); + + final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); + Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm, RoundingMode.HALF_EVEN); + + Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()), RoundingMode.HALF_EVEN); + + Money principalPerInstallment = loanScheduleInfo.getPrincipal() + .dividedBy(loanScheduleInfo.getNumberOfRepayments(), + RoundingMode.HALF_EVEN); + + Money outstandingBalance = loanScheduleInfo.getPrincipal(); + Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); + Money totalInterest = Money.zero(outstandingBalance.getCurrency()); + + // create entries of disbursement period on loan schedule + final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursement(disbursementDate, loanScheduleInfo.getPrincipal().getAmount()); + periods.add(disbursementPeriod); + + int loanTermInDays = Integer.valueOf(0); + BigDecimal cumulativePrincipalDisbursed = loanScheduleInfo.getPrincipal().getAmount(); + BigDecimal cumulativePrincipalDue = BigDecimal.ZERO; + BigDecimal cumulativeInterestExpected = BigDecimal.ZERO; + BigDecimal cumulativeChargesToDate = BigDecimal.ZERO; + BigDecimal totalExpectedRepayment = BigDecimal.ZERO; + + LocalDate startDate = disbursementDate; + int periodNumber = 1; + for (LocalDate scheduledDueDate : scheduledDates) { + totalPrincipal = totalPrincipal.plus(principalPerInstallment); + totalInterest = totalInterest.plus(interestPerInstallment); + + // number of days from startDate to this scheduledDate + int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays(); + + if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { + final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); + if (difference.isLessThanZero()) { + principalPerInstallment = principalPerInstallment.plus(difference.abs()); + } else if (difference.isGreaterThanZero()) { + principalPerInstallment = principalPerInstallment.minus(difference.abs()); + } + + final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm); + if (interestDifference.isLessThanZero()) { + interestPerInstallment = interestPerInstallment.plus(interestDifference.abs()); + } else if (interestDifference.isGreaterThanZero()) { + interestPerInstallment = interestPerInstallment.minus(interestDifference.abs()); + } + } + + Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment); + outstandingBalance = outstandingBalance.minus(principalPerInstallment); + + LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentPeriod(periodNumber, startDate, + scheduledDueDate, + principalPerInstallment.getAmount(), + outstandingBalance.getAmount(), + interestPerInstallment.getAmount(), totalInstallmentDue.getAmount()); + + periods.add(installment); + + // handle cumulative fields + loanTermInDays += daysInPeriod; + cumulativePrincipalDue = cumulativePrincipalDue.add(principalPerInstallment.getAmount()); + cumulativeInterestExpected = cumulativeInterestExpected.add(interestPerInstallment.getAmount()); + totalExpectedRepayment = totalExpectedRepayment.add(totalInstallmentDue.getAmount()); + startDate = scheduledDueDate; + + periodNumber++; + } + + final BigDecimal cumulativePrincipalOutstanding = cumulativePrincipalDisbursed.subtract(cumulativePrincipalDue); + + CurrencyData currencyData = new CurrencyData( + currency.getCode(), + currency.getName(), + monetaryCurrency.getDigitsAfterDecimal(), + currency.getDisplaySymbol(), + currency.getNameCode()); + + return new NewLoanScheduleData(currencyData, periods, loanTermInDays, cumulativePrincipalDisbursed, cumulativePrincipalDue, + cumulativePrincipalOutstanding, cumulativeInterestExpected, cumulativeChargesToDate, totalExpectedRepayment); } @Override diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java index c1439e7aa..12a55c391 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java @@ -4,17 +4,23 @@ import org.joda.time.LocalDate; import org.mifosng.platform.api.NewLoanScheduleData; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.LoanSchedule; +import org.mifosng.platform.currency.domain.ApplicationCurrency; import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; import org.mifosng.platform.loan.domain.PeriodFrequencyType; public interface LoanScheduleGenerator { + @Deprecated LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo, Integer loanTermFrequency, PeriodFrequencyType loanTermFrequencyType, LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, CurrencyData currencyData); - NewLoanScheduleData generate(LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom); + NewLoanScheduleData generate(ApplicationCurrency applicationCurrency, + LoanProductRelatedDetail loanScheduleInfo, + Integer loanTermFrequency, + PeriodFrequencyType loanTermFrequencyType, + LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom); } \ No newline at end of file