tidy up calculate loan schedule validation.

This commit is contained in:
Keith Woodlock
2012-05-29 17:36:28 +01:00
parent 1450e38f7b
commit d92a2b1f25
16 changed files with 622 additions and 721 deletions
@@ -19,8 +19,6 @@ import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.data.command.ImportLoanCommand;
import org.mifosng.data.command.ImportLoanRepaymentsCommand;
@@ -32,25 +30,21 @@ import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.commands.ImportClientCommand;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.client.domain.ClientRepository;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.UnAuthenticatedUserException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine;
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.LoanLifecycleStateMachine;
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.LoanRepaymentScheduleInstallment;
import org.mifosng.platform.loan.domain.LoanRepository;
import org.mifosng.platform.loan.domain.LoanStatus;
import org.mifosng.platform.loan.domain.LoanStatusRepository;
import org.mifosng.platform.loan.domain.LoanTransaction;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
import org.mifosng.platform.loan.service.CalculationPlatformService;
import org.mifosng.platform.loan.service.LoanAssembler;
import org.mifosng.platform.organisation.domain.Office;
import org.mifosng.platform.organisation.domain.OfficeRepository;
import org.mifosng.platform.organisation.domain.Organisation;
@@ -75,11 +69,13 @@ public class ImportPlatformServiceImpl implements ImportPlatformService {
private final CalculationPlatformService calculationPlatformService;
private final LoanRepository loanRepository;
private final LoanStatusRepository loanStatusRepository;
private final LoanAssembler loanAssembler;
@Autowired
public ImportPlatformServiceImpl(
public ImportPlatformServiceImpl(final LoanAssembler loanAssembler,
final OfficeRepository officeRepository, final ClientRepository clientRepository, final LoanProductRepository loanProductRepository,
final CalculationPlatformService calculationPlatformService, final LoanRepository loanRepository, final LoanStatusRepository loanStatusRepository) {
this.loanAssembler = loanAssembler;
this.officeRepository = officeRepository;
this.clientRepository = clientRepository;
this.loanProductRepository = loanProductRepository;
@@ -138,75 +134,10 @@ public class ImportPlatformServiceImpl implements ImportPlatformService {
SubmitLoanApplicationCommand command = combinedCommand.getSubmitLoanApplicationCommand();
// most of this code can be extracted out into an Assembler
LoanProduct loanProduct = this.loanProductRepository.findOne(command
.getProductId());
Client client = this.clientRepository.findOne(command.getApplicantId());
MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command
.getInterestRatePerPeriod().doubleValue());
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
// apr calculator
BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO;
switch (interestPeriodFrequencyType) {
case DAYS:
break;
case WEEKS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(52));
break;
case MONTHS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(12));
break;
case YEARS:
defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod();
break;
case INVALID:
break;
}
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodMethod());
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType
.fromInt(command.getRepaymentFrequency());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency, command.getPrincipal(),
defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceAmount());
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule
.getScheduledLoanInstallments();
Loan loan = Loan.createNew(currentUser.getOrganisation(), loanProduct, client, loanRepaymentScheduleDetail);
loan.setExternalId(command.getExternalId());
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
MoneyData readPrincipalDue = scheduledLoanInstallment
.getPrincipalDue();
MoneyData readInterestDue = scheduledLoanInstallment
.getInterestDue();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(
loan, scheduledLoanInstallment.getInstallmentNumber(),
scheduledLoanInstallment.getPeriodStart(),
scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(),
readInterestDue.getAmount());
loan.addRepaymentScheduleInstallment(installment);
}
Loan loan = loanAssembler.assembleFrom(command, currentUser.getOrganisation());
LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
loan.submitApplication(command.getSubmittedOnDate(), command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), loanLifecycleStateMachine);
LoanStateTransitionCommand approveLoanCommand = combinedCommand.getApproveLoanCommand();
if (approveLoanCommand != null) {
loan.approve(approveLoanCommand.getEventDate(), loanLifecycleStateMachine);
@@ -109,42 +109,42 @@ public class LoansApiResource {
@QueryParam("command") final String commandParam,
final SubmitLoanApplicationCommand command) {
LocalDate expectedDisbursementDate = apiDataConversionService
.convertFrom(command.getExpectedDisbursementDateFormatted(),
"expectedDisbursementDateFormatted",
LocalDate expectedDisbursementLocalDate = apiDataConversionService
.convertFrom(command.getExpectedDisbursementDate(),
"expectedDisbursementDate",
command.getDateFormat());
LocalDate repaymentsStartingFromDate = apiDataConversionService
.convertFrom(command.getRepaymentsStartingFromDateFormatted(),
"repaymentsStartingFromDateFormatted",
LocalDate repaymentsStartingFromLocalDate = apiDataConversionService
.convertFrom(command.getRepaymentsStartingFromDate(),
"repaymentsStartingFromDate",
command.getDateFormat());
LocalDate interestCalculatedFromDate = apiDataConversionService
.convertFrom(command.getInterestCalculatedFromDateFormatted(),
"interestCalculatedFromDateFormatted",
LocalDate interestChargedFromLocalDate = apiDataConversionService
.convertFrom(command.getInterestChargedFromDate(),
"interestChargedFromDate",
command.getDateFormat());
LocalDate submittedOnDate = apiDataConversionService.convertFrom(
command.getSubmittedOnDateFormatted(),
"submittedOnDateFormatted", command.getDateFormat());
command.setExpectedDisbursementDate(expectedDisbursementDate);
command.setRepaymentsStartingFromDate(repaymentsStartingFromDate);
command.setInterestCalculatedFromDate(interestCalculatedFromDate);
command.setSubmittedOnDate(submittedOnDate);
LocalDate submittedOnLocalDate = apiDataConversionService.convertFrom(
command.getSubmittedOnDate(),
"submittedOnDate", command.getDateFormat());
command.setExpectedDisbursementLocalDate(expectedDisbursementLocalDate);
command.setRepaymentsStartingFromLocalDate(repaymentsStartingFromLocalDate);
command.setInterestChargedFromLocalDate(interestChargedFromLocalDate);
command.setSubmittedOnLocalDate(submittedOnLocalDate);
// FIXME - pass in locale through query string or in 'request body'
Locale clientApplicationLocale = Locale.UK;
BigDecimal principal = this.apiDataConversionService.convertFrom(
command.getPrincipalFormatted(), "principalFormatted",
clientApplicationLocale);
BigDecimal interestRatePerPeriod = this.apiDataConversionService
.convertFrom(command.getInterestRatePerPeriodFormatted(),
"interestRatePerPeriodFormatted",
clientApplicationLocale);
BigDecimal inArrearsToleranceAmount = this.apiDataConversionService
.convertFrom(command.getInArrearsToleranceAmountFormatted(),
"inArrearsToleranceAmountFormatted",
clientApplicationLocale);
command.setPrincipal(principal);
command.setInterestRatePerPeriod(interestRatePerPeriod);
command.setInArrearsToleranceAmount(inArrearsToleranceAmount);
Locale clientApplicationLocale = this.apiDataConversionService.localeFromString(command.getLocale());
BigDecimal principalValue = this.apiDataConversionService.convertFrom(command.getPrincipal(), "principal", clientApplicationLocale);
BigDecimal inArrearsToleranceValue = this.apiDataConversionService.convertFrom(command.getInArrearsTolerance(), "inArrearsTolerance", clientApplicationLocale);
BigDecimal interestRatePerPeriodValue = this.apiDataConversionService.convertFrom(command.getInterestRatePerPeriod(),"interestRatePerPeriod", clientApplicationLocale);
Integer digitsAfterDecimalValue = this.apiDataConversionService.convertToInteger(command.getDigitsAfterDecimal(), "digitsAfterDecimal", clientApplicationLocale);
Integer repaymentEveryValue = this.apiDataConversionService.convertToInteger(command.getRepaymentEvery(), "repaymentEvery", clientApplicationLocale);
Integer numberOfRepaymentsValue = this.apiDataConversionService.convertToInteger(command.getNumberOfRepayments(), "numberOfRepayments", clientApplicationLocale);
command.setPrincipalValue(principalValue);
command.setInArrearsToleranceValue(inArrearsToleranceValue);
command.setInterestRatePerPeriodValue(interestRatePerPeriodValue);
command.setDigitsAfterDecimalValue(digitsAfterDecimalValue);
command.setRepaymentEveryValue(repaymentEveryValue);
command.setNumberOfRepaymentsValue(numberOfRepaymentsValue);
CalculateLoanScheduleCommand calculateLoanScheduleCommand = command
.toCalculateLoanScheduleCommand();
@@ -99,7 +99,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
Money interest = getInterest(totalAvailable.getCurrency());
Money principal = getPrincipal(totalAvailable.getCurrency());
// TODO - configuration around order components of loan are paid off in.
// TODO - NEWFEATURE - configuration around order components of loan are paid off in.
// pay off interest
if (remaining.isGreaterThanOrEqualTo(interest)) {
this.interestCompleted = interest.getAmount();
@@ -3,13 +3,10 @@ package org.mifosng.platform.loan.service;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosng.data.ApiParameterError;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
import org.mifosng.platform.DataValidatorBuilder;
import org.mifosng.platform.exceptions.PlatformApiDataValidationException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.InterestMethod;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
public class CalculateLoanScheduleCommandValidator {
@@ -20,199 +17,53 @@ public class CalculateLoanScheduleCommandValidator {
}
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
if (StringUtils.isBlank(command.getCurrencyCode())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.currency.cannot.be.empty", "The parameter currencyCode cannot be blank.", "currencyCode");
dataValidationErrors.add(error);
}
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
if (command.getDigitsAfterDecimal() != null) {
if (command.getDigitsAfterDecimal() < 0 || command.getDigitsAfterDecimal() > 6) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.currency.digitsAfterDecimal.must.be.between.zero.and.six.inclusive", "The parameter digitsAfterDecimal must be a value between 1 and 6.", "digitsAfterDecimal", command.getDigitsAfterDecimal(), 1, 6);
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.currency.digitsAfterDecimal.cannot.be.empty", "The parameter digitsAfterDecimal cannot be blank.", "digitsAfterDecimal");
dataValidationErrors.add(error);
}
baseDataValidator.reset().parameter("currencyCode").value(command.getCurrencyCode()).notBlank();
baseDataValidator.reset().parameter("digitsAfterDecimal").value(command.getDigitsAfterDecimalValue()).notNull().inMinMaxRange(0, 6);
baseDataValidator.reset().parameter("principal").value(command.getPrincipalValue()).notNull().positiveAmount();
baseDataValidator.reset().parameter("repaymentFrequencyType").value(command.getRepaymentFrequencyType()).notNull().inMinMaxRange(0, 3);
baseDataValidator.reset().parameter("repaymentEvery").value(command.getRepaymentEveryValue()).notNull().greaterThanZero();
baseDataValidator.reset().parameter("numberOfRepayments").value(command.getNumberOfRepaymentsValue()).notNull().greaterThanZero();
baseDataValidator.reset().parameter("interestRatePerPeriod").value(command.getInterestRatePerPeriodValue()).notNull();
baseDataValidator.reset().parameter("interestRateFrequencyType").value(command.getInterestRateFrequencyType()).notNull().inMinMaxRange(0, 3);
baseDataValidator.reset().parameter("amortizationType").value(command.getAmortizationType()).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestType").value(command.getInterestType()).notNull().inMinMaxRange(0, 1);
baseDataValidator.reset().parameter("interestCalculationPeriodType").value(command.getInterestCalculationPeriodType()).notNull().inMinMaxRange(0, 1);
// FIXME - validate the number of digits before and after decimal provided (before is limited by database field, after is limited by number selected for product (and ultimately by the database field))
if (command.getPrincipal() != null) {
if (command.getPrincipal().doubleValue() <= Double.valueOf("0.0")) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.principal.amount.must.be.greater.than.zero", "The parameter principalMoney must be greater than zero.", "principalMoney");
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.principal.amount.cannot.be.empty", "The parameter principalMoney cannot be blank.", "principalMoney");
dataValidationErrors.add(error);
}
baseDataValidator.reset().parameter("expectedDisbursementDate").value(command.getExpectedDisbursementLocalDate()).notNull();
if (command.getRepaymentFrequency() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
ApiParameterError error = null;
switch (frequencyType) {
case DAYS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 365);
break;
case WEEKS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 52);
break;
case MONTHS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 12);
break;
case YEARS:
error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 2);
break;
default:
error = ApiParameterError.parameterError("validation.msg.product.repayment.frequency.invalid", "The parameter repaymentFrequency is invalid.", "repaymentFrequency", command.getRepaymentFrequency());
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.repayment.frequency.cannot.be.empty", "The parameter repaymentFrequency cannot be empty.", "repaymentFrequency");
dataValidationErrors.add(error);
}
if (command.getNumberOfRepayments() != null) {
if (command.getNumberOfRepayments() < 1 || command.getNumberOfRepayments() > 100) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.number.of.repayments.is.out.of.range", "The parameter numberOfRepayments must between {1} and {2}.", "numberOfRepayments", command.getNumberOfRepayments(), 1, 100);
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.number.of.repayments.cannot.be.empty", "The parameter numberOfRepayments cannot be empty.", "numberOfRepayments");
dataValidationErrors.add(error);
}
if (command.getAmortizationMethod() != null) {
AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
ApiParameterError error = null;
switch (amortizationMethod) {
case EQUAL_PRINCIPAL:
break;
case EQUAL_INSTALLMENTS:
break;
default:
error = ApiParameterError.parameterError("validation.msg.product.amortization.method.invalid", "The parameter amortizationMethod is invalid.", "amortizationMethod", command.getAmortizationMethod());
dataValidationErrors.add(error);
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.amortization.method.cannot.be.empty", "The parameter amortizationMethod cannot be empty.", "amortizationMethod");
dataValidationErrors.add(error);
}
if (command.getExpectedDisbursementDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.expectedDisbursementDate.method.cannot.be.blank", "The parameter expectedDisbursementDate cannot be empty.", "expectedDisbursementDate");
dataValidationErrors.add(error);
} else {
if (command.getRepaymentsStartingFromDate() != null
&& command.getExpectedDisbursementDate().isAfter(command.getRepaymentsStartingFromDate())) {
if (command.getExpectedDisbursementLocalDate() != null) {
if (command.getRepaymentsStartingFromLocalDate() != null
&& command.getExpectedDisbursementLocalDate().isAfter(command.getRepaymentsStartingFromLocalDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.expectedDisbursementDate.cannot.be.after.first.repayment.date",
"The parameter expectedDisbursementDate has a date which falls after the given first repayment date.", "expectedDisbursementDate",
command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate());
command.getExpectedDisbursementLocalDate(), command.getRepaymentsStartingFromLocalDate());
dataValidationErrors.add(error);
}
}
if (command.getRepaymentsStartingFromDate() != null && command.getInterestCalculatedFromDate() == null) {
if (command.getRepaymentsStartingFromLocalDate() != null && command.getInterestChargedFromLocalDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field",
"The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.", "interestCalculatedFromDate", command.getRepaymentsStartingFromDate());
"The parameter interestCalculatedFromDate cannot be empty when first repayment date is provided.", "interestCalculatedFromDate", command.getRepaymentsStartingFromLocalDate());
dataValidationErrors.add(error);
} else if (command.getRepaymentsStartingFromDate() == null && command.getInterestCalculatedFromDate() != null) {
} else if (command.getRepaymentsStartingFromLocalDate() == null && command.getInterestChargedFromLocalDate() != null) {
// validate interestCalculatedFromDate is after or on repaymentsStartingFromDate
if (command.getExpectedDisbursementDate().isAfter(command.getInterestCalculatedFromDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestCalculatedFromDate.cannot.be.before.disbursement.date",
"The parameter interestCalculatedFromDate cannot be before the date given for expected disbursement.", "interestCalculatedFromDate", command.getInterestCalculatedFromDate(), command.getExpectedDisbursementDate());
if (command.getExpectedDisbursementLocalDate().isAfter(command.getInterestChargedFromLocalDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.interestChargedFromDate.cannot.be.before.disbursement.date",
"The parameter interestCalculatedFromDate cannot be before the date given for expected disbursement.", "interestChargedFromDate", command.getInterestChargedFromLocalDate(), command.getExpectedDisbursementLocalDate());
dataValidationErrors.add(error);
}
}
if (command.getInterestRatePerPeriod()!= null) {
if (command.getInterestRatePerPeriod().doubleValue() < Double.valueOf("0.0")) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.interest.rate.per.period.amount.cannot.be.negative",
"The parameter interestRatePerPeriodFormatted cannot be less than zero.", "interestRatePerPeriodFormatted",
command.getInterestRatePerPeriod());
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.interest.rate.per.period.amount.cannot.be.empty",
"The parameter interestRatePerPeriodFormatted cannot be empty.", "interestRatePerPeriodFormatted",
command.getInterestRatePerPeriod());
dataValidationErrors.add(error);
}
// interest rate period type
if (command.getInterestRateFrequencyMethod() != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
ApiParameterError error = null;
switch (frequencyType) {
case DAYS:
break;
case WEEKS:
break;
case MONTHS:
break;
case YEARS:
break;
default:
error = ApiParameterError.parameterError("validation.msg.product.interest.frequency.invalid",
"The parameter interestRateFrequencyMethod cannot be empty.", "interestRateFrequencyMethod", command.getInterestRateFrequencyMethod());
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.interest.frequency.cannot.be.empty",
"The parameter interestRateFrequencyMethod cannot be empty.", "interestRateFrequencyMethod");
dataValidationErrors.add(error);
}
if (command.getInterestMethod() != null) {
InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
ApiParameterError error = null;
switch (interestMethod) {
case DECLINING_BALANCE:
break;
case FLAT:
break;
default:
error = ApiParameterError.parameterError("validation.msg.product.interest.method.invalid",
"The parameter interestMethod is invalid.", "interestMethod", command.getInterestMethod());
break;
}
if (error != null) {
dataValidationErrors.add(error);
}
} else {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.product.interest.method.cannot.be.empty",
"The parameter interestMethod cannot be empty.", "interestMethod");
dataValidationErrors.add(error);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
private ApiParameterError validateNumberExistsAndInRange(String errorCodePreFix, String fieldName, Integer value, int min, int max) {
ApiParameterError error = null;
if (value == null) {
error = ApiParameterError.parameterError(errorCodePreFix + ".cannot.be.empty", "The parameter " + fieldName + " cannot be blank.", fieldName);
} else {
if (value < min || value > max) {
error = ApiParameterError.parameterError(errorCodePreFix + ".outside.allowed.range", "The parameter " + fieldName + " must be between {0} and {1} inclusive.", fieldName, min, max);
}
}
return error;
}
}
@@ -51,20 +51,19 @@ public class CalculationPlatformServiceImpl implements
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command);
validator.validate();
// assemble 'loan data' into 'domain' object [LoanProductRelatedDetail]
final BigDecimal principalAmount = BigDecimal.valueOf(command.getPrincipal().doubleValue());
final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command.getInterestRatePerPeriod().doubleValue());
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodMethod());
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
final BigDecimal principalAmount = command.getPrincipalValue();
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriodValue();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType());
final Integer repayEvery = command.getRepaymentEveryValue();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
final Integer defaultNumberOfInstallments = command.getNumberOfRepaymentsValue();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestPeriodFrequencyType, defaultNominalInterestRatePerPeriod);
final MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
final MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimalValue());
// in arrerars tolerance isnt relevant when auto-calculating a loan schedule
final BigDecimal inArrearsTolerance = BigDecimal.ZERO;
@@ -78,11 +77,11 @@ public class CalculationPlatformServiceImpl implements
LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod);
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(command.getCurrencyCode());
CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), command.getDigitsAfterDecimal(),
CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), command.getDigitsAfterDecimalValue(),
applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode());
return loanScheduleGenerator.generate(loanScheduleInfo, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(),
command.getInterestCalculatedFromDate(), currencyData);
return loanScheduleGenerator.generate(loanScheduleInfo, command.getExpectedDisbursementLocalDate(), command.getRepaymentsStartingFromLocalDate(),
command.getInterestChargedFromLocalDate(), currencyData);
}
@Override
@@ -0,0 +1,112 @@
package org.mifosng.platform.loan.service;
import static org.mifosng.platform.Specifications.clientsThatMatch;
import static org.mifosng.platform.Specifications.productThatMatches;
import java.math.BigDecimal;
import java.util.List;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.data.command.SubmitLoanApplicationCommand;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.client.domain.ClientRepository;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.LoanProductNotFoundException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine;
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.LoanLifecycleStateMachine;
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.LoanRepaymentScheduleInstallment;
import org.mifosng.platform.loan.domain.LoanStatus;
import org.mifosng.platform.loan.domain.LoanStatusRepository;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
import org.mifosng.platform.loanschedule.domain.AprCalculator;
import org.mifosng.platform.organisation.domain.Organisation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LoanAssembler {
private final LoanStatusRepository loanStatusRepository;
private final LoanProductRepository loanProductRepository;
private final ClientRepository clientRepository;
private final AprCalculator aprCalculator = new AprCalculator();
@Autowired
public LoanAssembler(final LoanStatusRepository loanStatusRepository,
final LoanProductRepository loanProductRepository,
final ClientRepository clientRepository) {
this.loanStatusRepository = loanStatusRepository;
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
}
public Loan assembleFrom(SubmitLoanApplicationCommand command, Organisation organisation) {
LoanProduct loanProduct = this.loanProductRepository.findOne(productThatMatches(organisation, command.getProductId()));
if (loanProduct == null) {
throw new LoanProductNotFoundException(command.getProductId());
}
Client client = this.clientRepository.findOne(clientsThatMatch(organisation, command.getClientId()));
if (client == null) {
throw new ClientNotFoundException(command.getClientId());
}
// FIXME - should probably fetch applicationCurrency with currencycode to check its validity.
MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimalValue());
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriodValue();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
BigDecimal defaultAnnualNominalInterestRate = aprCalculator.calculateFrom(interestPeriodFrequencyType, command.getInterestRatePerPeriodValue());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestType());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodType());
final Integer repayEvery = command.getRepaymentEveryValue();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequencyType());
final Integer defaultNumberOfInstallments = command.getNumberOfRepaymentsValue();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationType());
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency,
command.getPrincipalValue(), defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate,
interestMethod, interestCalculationPeriodMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceValue());
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments();
Loan loan = Loan.createNew(organisation, loanProduct, client, loanRepaymentScheduleDetail);
loan.setExternalId(command.getExternalId());
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue();
MoneyData readInterestDue = scheduledLoanInstallment.getInterestDue();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(
loan, scheduledLoanInstallment.getInstallmentNumber(),
scheduledLoanInstallment.getPeriodStart(),
scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(),
readInterestDue.getAmount());
loan.addRepaymentScheduleInstallment(installment);
}
loan.submitApplication(command.getSubmittedOnLocalDate(), command.getExpectedDisbursementLocalDate(), command.getRepaymentsStartingFromLocalDate(), command.getInterestChargedFromLocalDate(), defaultLoanLifecycleStateMachine());
return loan;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
List<LoanStatus> allowedLoanStatuses = this.loanStatusRepository.findAll();
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
}
@@ -37,7 +37,9 @@ public interface LoanWritePlatformService {
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_IN_THE_PAST_ROLE')")
public EntityIdentifier makeLoanRepayment(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier waiveLoanAmount(LoanTransactionCommand command);
}
@@ -2,7 +2,6 @@ package org.mifosng.platform.loan.service;
import static org.mifosng.platform.Specifications.loansThatMatch;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -10,7 +9,6 @@ import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosng.data.EntityIdentifier;
import org.mifosng.data.LoanSchedule;
import org.mifosng.data.MoneyData;
import org.mifosng.data.ScheduledLoanInstallment;
import org.mifosng.data.command.AdjustLoanTransactionCommand;
import org.mifosng.data.command.CalculateLoanScheduleCommand;
@@ -18,8 +16,6 @@ import org.mifosng.data.command.LoanStateTransitionCommand;
import org.mifosng.data.command.LoanTransactionCommand;
import org.mifosng.data.command.SubmitLoanApplicationCommand;
import org.mifosng.data.command.UndoStateTransitionCommand;
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;
@@ -27,15 +23,9 @@ import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.LoanNotFoundException;
import org.mifosng.platform.exceptions.NoAuthorizationException;
import org.mifosng.platform.exceptions.PlatformDomainRuleException;
import org.mifosng.platform.loan.domain.AmortizationMethod;
import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine;
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.LoanLifecycleStateMachine;
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.LoanRepaymentScheduleInstallment;
import org.mifosng.platform.loan.domain.LoanRepository;
import org.mifosng.platform.loan.domain.LoanStatus;
@@ -43,7 +33,6 @@ import org.mifosng.platform.loan.domain.LoanStatusRepository;
import org.mifosng.platform.loan.domain.LoanTransaction;
import org.mifosng.platform.loan.domain.LoanTransactionRepository;
import org.mifosng.platform.loan.domain.PeriodFrequencyType;
import org.mifosng.platform.loanschedule.domain.AprCalculator;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
@@ -56,24 +45,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final PlatformSecurityContext context;
private final LoanRepository loanRepository;
private final LoanProductRepository loanProductRepository;
private final LoanStatusRepository loanStatusRepository;
private final ClientRepository clientRepository;
private final NoteRepository noteRepository;
private final CalculationPlatformService calculationPlatformService;
private final AprCalculator aprCalculator = new AprCalculator();
private final LoanTransactionRepository loanTransactionRepository;
private final LoanAssembler loanAssembler;
@Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final LoanAssembler loanAssembler,
final LoanRepository loanRepository, final LoanTransactionRepository loanTransactionRepository,
final LoanStatusRepository loanStatusRepository, final LoanProductRepository loanProductRepository,
final ClientRepository clientRepository, final NoteRepository noteRepository, final CalculationPlatformService calculationPlatformService) {
final LoanStatusRepository loanStatusRepository,
final NoteRepository noteRepository, final CalculationPlatformService calculationPlatformService) {
this.context = context;
this.loanAssembler = loanAssembler;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
this.loanStatusRepository = loanStatusRepository;
this.noteRepository = noteRepository;
this.calculationPlatformService = calculationPlatformService;
@@ -97,55 +83,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
SubmitLoanApplicationCommandValidator validator = new SubmitLoanApplicationCommandValidator(command);
validator.validate();
LocalDate submittedOn = command.getSubmittedOnDate();
LocalDate submittedOn = command.getSubmittedOnLocalDate();
if (this.isBeforeToday(submittedOn) && currentUser.hasNotPermissionForAnyOf("CAN_SUBMIT_HISTORIC_LOAN_APPLICATION_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE")) {
throw new NoAuthorizationException("Cannot add backdated loan.");
}
// FIXME - looks like assembly of LoanProductRelatedDetail is common with loan product and calculate loan schedule areas. remove duplication into an assembler.
LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
Client client = this.clientRepository.findOne(command.getApplicantId());
Loan loan = loanAssembler.assembleFrom(command, currentUser.getOrganisation());
MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal());
final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command.getInterestRatePerPeriod().doubleValue());
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod());
BigDecimal defaultAnnualNominalInterestRate = aprCalculator.calculateFrom(interestPeriodFrequencyType, command.getInterestRatePerPeriod());
final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod());
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(command.getInterestCalculationPeriodMethod());
final Integer repayEvery = command.getRepaymentEvery();
final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency());
final Integer defaultNumberOfInstallments = command.getNumberOfRepayments();
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod());
LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency,
command.getPrincipal(), defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate,
interestMethod, interestCalculationPeriodMethod,
repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceAmount());
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments();
Loan loan = Loan.createNew(currentUser.getOrganisation(), loanProduct, client, loanRepaymentScheduleDetail);
loan.setExternalId(command.getExternalId());
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue();
MoneyData readInterestDue = scheduledLoanInstallment.getInterestDue();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(
loan, scheduledLoanInstallment.getInstallmentNumber(),
scheduledLoanInstallment.getPeriodStart(),
scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(),
readInterestDue.getAmount());
loan.addRepaymentScheduleInstallment(installment);
}
loan.submitApplication(submittedOn, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), defaultLoanLifecycleStateMachine());
this.loanRepository.save(loan);
if (StringUtils.isNotBlank(command.getSubmittedOnNote())) {
@@ -25,15 +25,15 @@ public class SubmitLoanApplicationCommandValidator {
dataValidationErrors.add(error);
}
if (command.getSubmittedOnDate() == null) {
if (command.getSubmittedOnLocalDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.blank",
"The parameter submittedOnDateFormatted cannot be empty.", "submittedOnDateFormatted");
"The parameter submittedOnDate cannot be empty.", "submittedOnDateFormatted");
dataValidationErrors.add(error);
} else {
if (command.getSubmittedOnDate().isAfter(command.getExpectedDisbursementDate())) {
if (command.getSubmittedOnLocalDate().isAfter(command.getExpectedDisbursementLocalDate())) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.after.expectedDisbursementDate",
"The date of parameter submittedOnDateFormatted cannot fall after the date given for expectedDisbursementDateFormatted.", "submittedOnDateFormatted",
command.getSubmittedOnDateFormatted(), command.getExpectedDisbursementDateFormatted());
"The date of parameter submittedOnDate cannot fall after the date given for expectedDisbursementDate.", "submittedOnDate",
command.getSubmittedOnDate(), command.getExpectedDisbursementDate());
dataValidationErrors.add(error);
}
}
@@ -41,17 +41,17 @@ public class SubmitLoanApplicationCommandValidator {
try {
// reuse calculate loan schedule validator for now
CalculateLoanScheduleCommand calculateLoanScheduleCommand = new CalculateLoanScheduleCommand(
command.getCurrencyCode(), command.getDigitsAfterDecimal(),
command.getPrincipal(), command.getInterestRatePerPeriod(),
command.getInterestRateFrequencyMethod(),
command.getInterestMethod(), command.getInterestCalculationPeriodMethod(),
command.getRepaymentEvery(),
command.getRepaymentFrequency(),
command.getNumberOfRepayments(),
command.getAmortizationMethod(),
command.getExpectedDisbursementDate(),
command.getRepaymentsStartingFromDate(),
command.getInterestCalculatedFromDate());
command.getCurrencyCode(), command.getDigitsAfterDecimalValue(),
command.getPrincipalValue(), command.getInterestRatePerPeriodValue(),
command.getInterestRateFrequencyType(),
command.getInterestType(), command.getInterestCalculationPeriodType(),
command.getRepaymentEveryValue(),
command.getRepaymentFrequencyType(),
command.getNumberOfRepaymentsValue(),
command.getAmortizationType(),
command.getExpectedDisbursementLocalDate(),
command.getRepaymentsStartingFromLocalDate(),
command.getInterestChargedFromLocalDate());
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(
calculateLoanScheduleCommand);
@@ -9,7 +9,6 @@ import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class PmtCalculator {
public Money calculatePaymentForOnePeriodFrom(LoanProductRelatedDetail loanScheduleInfo, BigDecimal periodInterestRateForRepaymentPeriod, MonetaryCurrency monetaryCurrency) {
// TODO - check if going to doubles is a problem
double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue();
double futureValue = 0;
@@ -24,7 +23,6 @@ public class PmtCalculator {
}
public Money calculateTotalRepaymentFrom(LoanProductRelatedDetail loanScheduleInfo, BigDecimal periodInterestRateForRepaymentPeriod, MonetaryCurrency monetaryCurrency) {
// TODO - check if going to doubles is a problem
double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue();
double futureValue = 0;
@@ -44,7 +44,6 @@ public class RoleReadPlatformServiceImpl implements RoleReadPlatformService {
return this.jdbcTemplate.query(sql, mapper, new Object[] {currentUser.getOrganisation().getId() });
}
// FIXME - does it make sense to use JDBC over repository for this?
@Override
public RoleData retrieveRole(final Long id) {
AppUser currentUser = context.authenticatedUser();