WIP on support loan charges on loan schedule.

This commit is contained in:
Keith Woodlock
2012-09-18 15:15:30 +01:00
parent 56c8ce43cd
commit 6bd2d078a3
16 changed files with 238 additions and 335 deletions
@@ -270,14 +270,9 @@ public class LoansApiResource {
LoanApplicationCommand command = this.apiDataConversionService.convertJsonToLoanApplicationCommand(null, jsonRequestBody);
CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand();
LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand);
// 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();
NewLoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(calculateLoanScheduleCommand);
return Response.ok().entity(loanSchedule).build();
}
@@ -297,14 +292,13 @@ public class LoansApiResource {
final LoanApplicationCommand command = this.apiDataConversionService.convertJsonToLoanApplicationCommand(loanId, jsonRequestBody);
final CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand();
final LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand);
// for now just auto generating the loan schedule and setting support
// for 'manual' loan schedule creation later.
command.setLoanSchedule(loanSchedule);
// command.setLoanSchedule(loanSchedule);
if (is(commandParam, "calculateLoanSchedule")) {
final CalculateLoanScheduleCommand calculateLoanScheduleCommand = command.toCalculateLoanScheduleCommand();
final LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand);
return Response.ok().entity(loanSchedule).build();
}
@@ -4,7 +4,6 @@ import java.math.BigDecimal;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.data.LoanSchedule;
/**
* Immutable command for submitting new loan application.
@@ -44,8 +43,6 @@ public class LoanApplicationCommand {
private final LoanChargeCommand[] charges;
private LoanSchedule loanSchedule;
private final Set<String> modifiedParameters;
public LoanApplicationCommand(
@@ -104,14 +101,6 @@ public class LoanApplicationCommand {
this.loanTermFrequency, this.loanTermFrequencyType,
expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate);
}
public LoanSchedule getLoanSchedule() {
return loanSchedule;
}
public void setLoanSchedule(LoanSchedule loanSchedule) {
this.loanSchedule = loanSchedule;
}
public Long getLoanId() {
return loanId;
@@ -1,10 +1,9 @@
package org.mifosng.platform.api.data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class LoanSchedule implements Serializable {
public class LoanSchedule {
private List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
@@ -17,10 +16,6 @@ public class LoanSchedule implements Serializable {
return new CurrencyData(code, name, decimalPlaces, displaySymbol, nameCode);
}
public LoanSchedule() {
//
}
public LoanSchedule(final List<ScheduledLoanInstallment> scheduledLoanInstallments) {
this.scheduledLoanInstallments = scheduledLoanInstallments;
}
@@ -1,11 +1,10 @@
package org.mifosng.platform.api.data;
import java.io.Serializable;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
public class ScheduledLoanInstallment implements Serializable {
public class ScheduledLoanInstallment {
private Integer installmentNumber;
private LocalDate periodStart;
@@ -1,6 +1,7 @@
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;
@@ -8,6 +9,12 @@ public interface CalculationPlatformService {
LoanSchedule calculateLoanSchedule(CalculateLoanScheduleCommand command);
NewLoanScheduleData calculateLoanScheduleNew(CalculateLoanScheduleCommand command);
/**
* Not used at present from application.
*
* was a requirement from creocore that got into early prototye.
*/
LoanPayoffReadModel calculatePayoffOn(Long valueOf, LocalDate payoffDate);
}
@@ -3,6 +3,7 @@ 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;
@@ -32,8 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CalculationPlatformServiceImpl implements
CalculationPlatformService {
public class CalculationPlatformServiceImpl implements CalculationPlatformService {
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final LoanRepository loanRepository;
@@ -41,17 +41,54 @@ public class CalculationPlatformServiceImpl implements
private final ApplicationCurrencyRepository applicationCurrencyRepository;
private final AprCalculator aprCalculator;
private final PlatformSecurityContext context;
private final LoanProductRelatedDetailAssembler loanProductRelatedDetailAssembler;
@Autowired
public CalculationPlatformServiceImpl(final PlatformSecurityContext context,
final LoanRepository loanRepository, final LoanProductRepository loanProductRepository,
final ApplicationCurrencyRepository applicationCurrencyRepository, final AprCalculator aprCalculator) {
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;
}
@Override
public NewLoanScheduleData calculateLoanScheduleNew(final CalculateLoanScheduleCommand command) {
context.authenticatedUser();
CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command);
validator.validate();
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
@@ -144,4 +181,5 @@ public class CalculationPlatformServiceImpl implements
totalInterestOutstandingBasedOnExpectedMaturityDate,
totalInterestOutstandingBasedOnPayoffDate, interestRebateOwed);
}
}
@@ -21,11 +21,11 @@ public class LoanApplicationCommandValidator {
public void validate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
if (command.getLoanSchedule() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.submit.loan.loan.schedule.cannot.be.blank",
"The parameter loanSchedule cannot be empty.", "loanSchedule");
dataValidationErrors.add(error);
}
// if (command.getLoanSchedule() == null) {
// ApiParameterError error = ApiParameterError.parameterError("validation.msg.submit.loan.loan.schedule.cannot.be.blank",
// "The parameter loanSchedule cannot be empty.", "loanSchedule");
// dataValidationErrors.add(error);
// }
if (command.getSubmittedOnDate() == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.submitted.on.date.cannot.be.blank",
@@ -1,6 +1,7 @@
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;
@@ -8,7 +9,6 @@ import java.util.Set;
import org.mifosng.platform.api.commands.LoanApplicationCommand;
import org.mifosng.platform.api.commands.LoanChargeCommand;
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.charge.domain.Charge;
@@ -108,8 +108,9 @@ public class LoanAssembler {
loan.setExternalId(command.getExternalId());
// TODO - user service to calculate loan schedule at this point
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments();
// LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = new ArrayList<ScheduledLoanInstallment>();
// loanSchedule.getScheduledLoanInstallments();
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.util.List;
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.loan.domain.LoanProductRelatedDetail;
@@ -16,4 +17,13 @@ public interface AmortizationLoanScheduleGenerator {
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
List<LocalDate> scheduledDates);
NewLoanScheduleData generate(
LoanProductRelatedDetail loanScheduleInfo,
LocalDate disbursementDate,
LocalDate firstRepaymentDate,
LocalDate interestCalculatedFrom,
BigDecimal periodInterestRateForRepaymentPeriod,
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
List<LocalDate> scheduledDates);
}
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.util.List;
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.loan.domain.AmortizationMethod;
@@ -33,6 +34,34 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory();
@Override
public NewLoanScheduleData generate(
final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate,
final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom) {
// 1. generate valid set of 'due dates' based on some of the 'loan attributes'
List<LocalDate> scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates);
// 2. determine the 'periodic' interest rate based on the 'repayment periods' so we can use
final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo);
// Determine with 'amortisation' approach to use
AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo.getAmortizationMethod());
return generator.generate(
loanScheduleInfo,
disbursementDate,
firstRepaymentDate,
interestCalculatedFrom,
periodInterestRateForRepaymentPeriod,
idealDisbursementDateBasedOnFirstRepaymentDate,
scheduledDates);
}
@Override
public LoanSchedule generate(
final LoanProductRelatedDetail loanScheduleInfo,
@@ -57,133 +86,4 @@ public class DecliningBalanceMethodLoanScheduleGenerator implements LoanSchedule
return generator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData,
periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates);
}
/**
* Future value of an amount given the number of payments, rate, amount of
* individual payment, present value and boolean value indicating whether
* payments are due at the beginning of period (false => payments are due at
* end of period)
*
* @param r
* rate
* @param n
* num of periods
* @param y
* pmt per period
* @param p
* future value
* @param t
* type (true=pmt at end of period, false=pmt at begining of
* period)
*/
public static double fv(double r, double n, double y, double p, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * (p + (n * y));
} else {
double r1 = r + 1;
retval = ((1 - Math.pow(r1, n)) * (t ? r1 : 1) * y) / r - p
* Math.pow(r1, n);
}
return retval;
}
/**
* Present value of an amount given the number of future payments, rate,
* amount of individual payment, future value and boolean value indicating
* whether payments are due at the beginning of period (false => payments
* are due at end of period)
*
* @param r
* @param n
* @param y
* @param f
* @param t
*/
public static double pv(double r, double n, double y, double f, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * ((n * y) + f);
} else {
double r1 = r + 1;
retval = (((1 - Math.pow(r1, n)) / r) * (t ? r1 : 1) * y - f)
/ Math.pow(r1, n);
}
return retval;
}
/**
* calculates the Net Present Value of a principal amount given the discount
* rate and a sequence of cash flows (supplied as an array). If the amounts
* are income the value should be positive, else if they are payments and
* not income, the value should be negative.
*
* @param r
* @param cfs
* cashflow amounts
*/
public static double npv(double r, double[] cfs) {
double npv = 0;
double r1 = r + 1;
double trate = r1;
for (int i = 0, iSize = cfs.length; i < iSize; i++) {
npv += cfs[i] / trate;
trate *= r1;
}
return npv;
}
/**
* PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest).
*
* This monthly payment c depends upon
* the monthly interest rate r (expressed as a fraction, not a percentage,
* i.e., divide the quoted yearly percentage rate by 100 and by 12 to obtain
* the monthly interest rate), the number of monthly payments N called the
* loan's term, and the amount borrowed P known as the loan's principal; c
* is given by the formula:
*
* c = (r / (1 - (1 + r)^-N))P
*
* @param interestRateFraction
* @param numberOfPayments
* @param principal
* @param futureValue
* @param type
*/
public static double pmt(double interestRateFraction, double numberOfPayments, double principal, double futureValue, boolean type) {
double payment = 0;
if (interestRateFraction == 0) {
payment = -1 * (futureValue + principal) / numberOfPayments;
} else {
double r1 = interestRateFraction + 1;
payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction
/ ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments)));
}
return payment;
}
/**
*
* @param r
* @param y
* @param p
* @param f
* @param t
*/
public static double nper(double r, double y, double p, double f, boolean t) {
double retval = 0;
if (r == 0) {
retval = -1 * (f + p) / y;
} else {
double r1 = r + 1;
double ryr = (t ? r1 : 1) * y / r;
double a1 = ((ryr - f) < 0) ? Math.log(f - ryr) : Math.log(ryr - f);
double a2 = ((ryr - f) < 0) ? Math.log(-p - ryr) : Math
.log(p + ryr);
double a3 = Math.log(r1);
retval = (a1 - a2) / a3;
}
return retval;
}
}
@@ -6,8 +6,10 @@ 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.MonetaryCurrency;
@@ -23,6 +25,109 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
private final PmtCalculator pmtCalculator = new PmtCalculator();
@Override
public NewLoanScheduleData generate(
final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate,
final LocalDate firstRepaymentDate,
final LocalDate interestCalculatedFrom,
final BigDecimal periodInterestRateForRepaymentPeriod,
final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
final List<LocalDate> scheduledDates) {
// 1. add disbursements to loan schedule
// 2.
List<LoanSchedulePeriodData> periods = new ArrayList<LoanSchedulePeriodData>();
// determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
final Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal());
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);
LocalDate startDate = disbursementDate;
int installmentNumber = 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);
totalInterestDue = totalInterestDue.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);
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
}
totalPrincipal = totalPrincipal.plus(principalForInstallment);
totalInterest = totalInterest.plus(interestForInstallment);
if (installmentNumber == 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());
}
final Money interestDifference = totalInterest.minus(totalInterestDue);
if (interestDifference.isLessThanZero()) {
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
interestForInstallment = interestForInstallment.minus(interestDifference.abs());
}
}
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
outstandingBalance = outstandingBalance.minus(principalForInstallment);
// MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalForInstallment.getAmount());
// MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestForInstallment.getAmount());
// MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
// MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
// ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
// Integer.valueOf(installmentNumber), startDate,
// scheduledDueDate, principalPerInstallmentValue,
// interestPerInstallmentValue,
// totalInstallmentDueValue,
// outstandingBalanceValue);
LoanSchedulePeriodData installment = new LoanSchedulePeriodData();
periods.add(installment);
startDate = scheduledDueDate;
installmentNumber++;
}
return null;
}
@Override
public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate,
@@ -1,133 +0,0 @@
package org.mifosng.platform.loanschedule.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate;
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.api.data.ScheduledLoanInstallment;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.loan.domain.LoanProductRelatedDetail;
public class EqualInstallmentsWithIrregularRepaymentScheduleAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator {
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
private final PmtCalculator pmtCalculator = new PmtCalculator();
@Override
public 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<LocalDate> scheduledDates) {
List<ScheduledLoanInstallment> scheduledLoanInstallments = new ArrayList<ScheduledLoanInstallment>();
// 3. determine 'total payment' for each repayment based on pmt function (and hence the total due overall)
final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency();
Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, periodInterestRateForRepaymentPeriod, monetaryCurrency);
// REVIEW. calculate total interest due based on loan term settings rather than inferring loan term from regular repayment schedule
// final BigDecimal periodInterestRateForLoanTermPeriod = this.periodicInterestRateCalculator.calculateFrom(loanTermFrequencyType, loanScheduleInfo.getAnnualNominalInterestRate());
// final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo.getPrincipal(), loanTermFrequency, periodInterestRateForLoanTermPeriod, monetaryCurrency);
// totalDuePerInstallment = totalRepaymentDueForLoanTerm.dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN);
Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal());
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);
LocalDate startDate = disbursementDate;
int installmentNumber = 1;
for (LocalDate scheduledDueDate : scheduledDates) {
// number of days from startDate to this scheduledDate
int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()).getDays();
// TODO - total interest for loan is calculated using loan term settings (loanTermFrequency & loanTermFrequencyType)
// - its possible that the 'repyament schedule' is irregular in which case the interest for the installment might not be the periodic interest rate
// - but infact somethink like twice it e.g. 4 month installment followed by 2 month installment followed by 1 month installment
// - the below doesnt really support highly irregular loan schedules and this case needs to be looked at.
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);
totalInterestDue = totalInterestDue.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);
totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod);
interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0");
}
totalPrincipal = totalPrincipal.plus(principalForInstallment);
totalInterest = totalInterest.plus(interestForInstallment);
if (installmentNumber == 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());
}
final Money interestDifference = totalInterest.minus(totalInterestDue);
if (interestDifference.isLessThanZero()) {
interestForInstallment = interestForInstallment.plus(interestDifference.abs());
} else if (interestDifference.isGreaterThanZero()) {
interestForInstallment = interestForInstallment.minus(interestDifference.abs());
}
// if (loanScheduleInfo.getAmortizationMethod().equals(AmortizationMethod.EQUAL_INSTALLMENTS)) {
// Money interestDifference = totalInterest.minus(totalInterestDue);
// if (interestDifference.isLessThanZero()) {
// interestForInstallment = interestForInstallment.plus(interestDifference.abs());
// } else if (interestDifference.isGreaterThanZero()) {
// interestForInstallment = interestForInstallment.minus(interestDifference.abs());
// }
// }
}
Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment);
outstandingBalance = outstandingBalance.minus(principalForInstallment);
MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalForInstallment.getAmount());
MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestForInstallment.getAmount());
MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount());
MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount());
ScheduledLoanInstallment installment = new ScheduledLoanInstallment(
Integer.valueOf(installmentNumber), startDate,
scheduledDueDate, principalPerInstallmentValue,
interestPerInstallmentValue,
totalInstallmentDueValue,
outstandingBalanceValue);
scheduledLoanInstallments.add(installment);
startDate = scheduledDueDate;
installmentNumber++;
}
return new LoanSchedule(scheduledLoanInstallments);
}
}
@@ -6,6 +6,7 @@ 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.MoneyData;
@@ -19,6 +20,18 @@ public class EqualPrincipalAmortizationLoanScheduleGenerator implements Amortiza
private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public NewLoanScheduleData generate(
LoanProductRelatedDetail loanScheduleInfo,
LocalDate disbursementDate, LocalDate firstRepaymentDate,
LocalDate interestCalculatedFrom,
BigDecimal periodInterestRateForRepaymentPeriod,
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate,
List<LocalDate> scheduledDates) {
// TODO Auto-generated method stub
return null;
}
@Override
public LoanSchedule generate(final LoanProductRelatedDetail loanScheduleInfo,
final LocalDate disbursementDate,
@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.List;
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.MoneyData;
@@ -19,7 +20,15 @@ public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override
public NewLoanScheduleData generate(
LoanProductRelatedDetail loanScheduleInfo,
LocalDate disbursementDate, LocalDate firstRepaymentDate,
LocalDate interestCalculatedFrom) {
return null;
}
@Override
public LoanSchedule generate(
final LoanProductRelatedDetail loanScheduleInfo,
@@ -1,6 +1,7 @@
package org.mifosng.platform.loanschedule.domain;
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.loan.domain.LoanProductRelatedDetail;
@@ -14,4 +15,6 @@ public interface LoanScheduleGenerator {
LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom,
CurrencyData currencyData);
}
NewLoanScheduleData generate(LoanProductRelatedDetail loanScheduleInfo, LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom);
}
@@ -1,27 +0,0 @@
package org.mifosng.platform.loanschedule.domain;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class PmtTest {
@Test
public void shouldCalculatePmt() {
double interestRateFraction = Double.valueOf("0.0175");
double numberOfPayments = Double.valueOf("12");
double principal = Double.valueOf("-144300");
double futureValue = Double.valueOf("0");
boolean type = false;
double payment = DecliningBalanceMethodLoanScheduleGenerator.pmt(interestRateFraction, numberOfPayments, principal, futureValue, type);
assertThat(payment, is(Double.valueOf("13436.317553939027")));
}
}