Merge pull request #1059 from pramodn02/develop

MIFOSX-1526 : added future instalments calculation
This commit is contained in:
Vishwas Babu A J
2014-09-09 07:30:34 -07:00
12 changed files with 331 additions and 54 deletions
@@ -0,0 +1,14 @@
CREATE TABLE `m_loan_transaction_temp` (
`id` BIGINT(20) NOT NULL,
`loan_id` BIGINT(20) NOT NULL,
`amount` DECIMAL(19,6) NOT NULL DEFAULT '0',
`transaction_date` DATE NOT NULL
);
INSERT INTO m_loan_transaction_temp(`id`,`loan_id`,`transaction_date`,`amount`) select lt.id, lt.loan_id,lt.transaction_date,if(lt.transaction_type_enum = 1 , IFNULL(lt.amount,0),IFNULL(-lt.principal_portion_derived,0)) from m_loan_transaction lt where lt.is_reversed=0;
UPDATE m_loan_transaction lt SET lt.outstanding_loan_balance_derived = (select sum(ltt.amount) from m_loan_transaction_temp ltt where ltt.id <= lt.id and ltt.transaction_date <= lt.transaction_date and ltt.loan_id = lt.loan_id) where lt.transaction_type_enum != 10 and lt.is_reversed = 0;
DROP TABLE `m_loan_transaction_temp`;
@@ -9,7 +9,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -319,8 +318,8 @@ public class LoansApiResource {
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("repaymentSchedule", "transactions", "charges", "guarantors", "collateral",
"notes", "linkedAccount", "multiDisburseDetails"));
associationParameters.addAll(Arrays.asList("repaymentSchedule", "futureSchedule", "transactions", "charges", "guarantors",
"collateral", "notes", "linkedAccount", "multiDisburseDetails"));
}
if (associationParameters.contains("guarantors")) {
@@ -356,6 +355,11 @@ public class LoansApiResource {
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = loanBasicDetails.repaymentScheduleRelatedData();
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(loanId, repaymentScheduleRelatedData,
disbursementData);
if (associationParameters.contains("futureSchedule")) {
mandatoryResponseParameters.add("futureSchedule");
this.calculationPlatformService.updateFutureSchedule(repaymentSchedule, loanId);
}
}
if (associationParameters.contains("charges")) {
@@ -2095,6 +2095,7 @@ public class Loan extends AbstractPersistable<Long> {
chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero);
chargesPayment.updateLoan(this);
this.loanTransactions.add(chargesPayment);
updateLoanOutstandingBalaces();
}
}
@@ -2139,6 +2140,7 @@ public class Loan extends AbstractPersistable<Long> {
chargesPayment.updateComponents(zero, zero, charge.getAmount(getCurrency()), zero);
chargesPayment.updateLoan(this);
this.loanTransactions.add(chargesPayment);
updateLoanOutstandingBalaces();
charge.markAsFullyPaid();
return chargesPayment;
}
@@ -2493,7 +2495,7 @@ public class Loan extends AbstractPersistable<Long> {
return repaymentsOrWaivers;
}
private List<LoanTransaction> retreiveListOfTransactionsPostDisbursementExcludeAccruals() {
public List<LoanTransaction> retreiveListOfTransactionsPostDisbursementExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (!transaction.isDisbursement() && transaction.isNotReversed() && !transaction.isAccrual()) {
@@ -4111,14 +4113,54 @@ public class Loan extends AbstractPersistable<Long> {
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final CalendarInstance calendarInstanceForInterestRecalculation, LocalDate recalculateFrom) {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa) { return; }
final LoanScheduleModel loanSchedule = getRecalculatedSchedule(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation,
recalculateFrom);
if (loanSchedule == null) { return; }
updateLoanSchedule(loanSchedule);
LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate();
Set<LoanCharge> charges = this.charges();
for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
if (loanCharge.getDueLocalDate() == null || !lastRepaymentDate.isBefore(loanCharge.getDueLocalDate())) {
recalculateLoanCharge(loanCharge);
loanCharge.updateWaivedAmount(getCurrency());
} else {
loanCharge.setActive(false);
}
}
}
updateLoanSummaryDerivedFields();
}
private LoanScheduleModel getRecalculatedSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final CalendarInstance calendarInstanceForInterestRecalculation, LocalDate recalculateFrom) {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa) { return null; }
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(applicationCurrency,
calculatedRepaymentsStartingFromDate, calendarInstanceForInterestRecalculation);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final LoanScheduleModel loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mc, applicationCurrency,
loanApplicationTerms, charges(), isHolidayEnabled, holidays, workingDays,
retreiveListOfTransactionsPostDisbursementExcludeAccruals(), loanRepaymentScheduleTransactionProcessor,
this.repaymentScheduleInstallments, recalculateFrom);
return loanSchedule;
}
public LoanApplicationTerms constructLoanApplicationTerms(final ApplicationCurrency applicationCurrency,
final LocalDate calculatedRepaymentsStartingFromDate, final CalendarInstance calendarInstanceForInterestRecalculation) {
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
final List<DisbursementData> disbursementData = new ArrayList<>();
@@ -4149,30 +4191,7 @@ public class Loan extends AbstractPersistable<Long> {
this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, this.maxOutstandingLoanBalance,
loanVariationTermsData, getInterestChargedFromDate(), this.loanInterestRecalculationDetails,
calendarInstanceForInterestRecalculation, recalculationFrequencyType);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final LoanScheduleModel loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mc, applicationCurrency,
loanApplicationTerms, charges(), isHolidayEnabled, holidays, workingDays,
retreiveListOfTransactionsPostDisbursementExcludeAccruals(), loanRepaymentScheduleTransactionProcessor,
this.repaymentScheduleInstallments, recalculateFrom);
if (loanSchedule == null) { return; }
updateLoanSchedule(loanSchedule);
LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate();
Set<LoanCharge> charges = this.charges();
for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
if (loanCharge.getDueLocalDate() == null || !lastRepaymentDate.isBefore(loanCharge.getDueLocalDate())) {
recalculateLoanCharge(loanCharge);
loanCharge.updateWaivedAmount(getCurrency());
} else {
loanCharge.setActive(false);
}
}
}
updateLoanSummaryDerivedFields();
return loanApplicationTerms;
}
public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) {
@@ -4182,6 +4201,9 @@ public class Loan extends AbstractPersistable<Long> {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
final List<DisbursementData> disbursementData = new ArrayList<>();
@@ -4209,8 +4231,9 @@ public class Loan extends AbstractPersistable<Long> {
loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData,
this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate());
installment = loanScheduleGenerator.calculatePrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(),
loanApplicationTerms, LocalDate.now());
LocalDate.now(), getInterestChargedFromDate(), loanApplicationTerms, mc);
} else {
installment = this.getTotalOutstandingOnLoan();
}
@@ -4258,4 +4281,12 @@ public class Loan extends AbstractPersistable<Long> {
}
}
public LoanTransactionProcessingStrategy transactionProcessingStrategy() {
return this.transactionProcessingStrategy;
}
public boolean isNpa() {
return this.isNpa;
}
}
@@ -49,6 +49,8 @@ public class LoanScheduleData {
*/
private final Collection<LoanSchedulePeriodData> periods;
private Collection<LoanSchedulePeriodData> futurePeriods;
public LoanScheduleData(final CurrencyData currency, final Collection<LoanSchedulePeriodData> periods, final Integer loanTermInDays,
final BigDecimal totalPrincipalDisbursed, final BigDecimal totalPrincipalExpected, final BigDecimal totalPrincipalPaid,
final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, final BigDecimal totalPenaltyChargesCharged,
@@ -76,4 +78,8 @@ public class LoanScheduleData {
public Collection<LoanSchedulePeriodData> getPeriods() {
return this.periods;
}
public void updateFuturePeriods(Collection<LoanSchedulePeriodData> futurePeriods) {
this.futurePeriods = futurePeriods;
}
}
@@ -414,4 +414,8 @@ public class LoanSchedulePeriodData {
public BigDecimal totalOverdue() {
return defaultToZeroIfNull(this.totalOverdue);
}
public BigDecimal principalLoanBalanceOutstanding() {
return this.principalLoanBalanceOutstanding;
}
}
@@ -241,7 +241,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Money interestForThisinstallment = principalInterestForThisPeriod.interest();
if (daysCalcForInstallmentNumber == periodNumber) {
interestForThisinstallment = interestForThisinstallment.zero().plus(
calculateInterestForDays(daysInPeriodApplicableInFullInstallment, interestForThisinstallment,
calculateInterestForDays(daysInPeriodApplicableInFullInstallment, interestForThisinstallment.getAmount(),
daysInPeriodApplicableForInterest));
}
Money principalForThisPeriod = principalDisbursed.zero();
@@ -481,13 +481,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
daysInPeriodApplicableForInterest, amount);
int days = Days.daysBetween(startDate, endDate).getDays();
double interest = calculateInterestForDays(daysInPeriodApplicableForInterest, principalInterest.interest(), days);
double interest = calculateInterestForDays(daysInPeriodApplicableForInterest, principalInterest.interest().getAmount(), days);
return BigDecimal.valueOf(interest);
}
private double calculateInterestForDays(int daysInPeriodApplicableForInterest, Money interest, int days) {
return ((interest.getAmount().doubleValue()) / daysInPeriodApplicableForInterest) * days;
private double calculateInterestForDays(int daysInPeriodApplicableForInterest, BigDecimal interest, int days) {
if (interest.doubleValue() == 0) { return 0; }
return ((interest.doubleValue()) / daysInPeriodApplicableForInterest) * days;
}
public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator,
@@ -671,7 +672,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
List<LoanRepaymentScheduleInstallment> installments = previousSchedule;
List<LoanRepaymentScheduleInstallment> removeInstallments = new ArrayList<>();
MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency();
LoanTransaction preCloseTransaction = getPreclosureTransaction(transactions, installments, currency, loanApplicationTerms);
LoanTransaction preCloseTransaction = getPreclosureTransaction(transactions, installments, currency, loanApplicationTerms, mc);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isRecalculatedInterestComponent() && recalculateFrom != null
&& !recalculateFrom.isAfter(installment.getDueDate())) {
@@ -846,7 +847,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (recalculatedSchedule.getLoanScheduleModel() != null) {
loanScheduleModel = recalculatedSchedule.getLoanScheduleModel();
installments = retrieveRepaymentSchedule(loanScheduleModel);
lastInstallmentNumber = installments.size() - 1;
lastInstallmentNumber = installments.size();
}
}
return loanScheduleModel;
@@ -963,7 +964,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
}
if (!diffAmt.isEmpty() && skipRecalculation <= installment.getInstallmentNumber()) {
if ((!diffAmt.isEmpty() && skipRecalculation < installment.getInstallmentNumber())
|| skipRecalculation == installment.getInstallmentNumber()) {
processRecalculate = true;
}
break;
@@ -1039,15 +1041,19 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
*/
@Override
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, final LoanApplicationTerms applicationTerms, final LocalDate onDate) {
Money outstandingPrincipal = Money.zero(currency);
MonetaryCurrency currency, final LocalDate onDate, LocalDate interestChargedFromLocalDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc) {
Money feeCharges = Money.zero(currency);
Money penaltyCharges = Money.zero(currency);
Money totalPrincipal = Money.zero(currency);
Money totalInterest = Money.zero(currency);
LocalDate calculateInterestFrom = onDate;
LocalDate dueDate = onDate;
Money inerestForCurrentInstallment = null;
LocalDate repaymentDueDate = null;
boolean isInterestRepayment = false;
Money inerestForCurrentInstallment = Money.zero(currency);
Money principalOutstanding = Money.zero(currency);
Integer periodNumber = 0;
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyPaidOff()) {
if (!currentInstallment.getDueDate().isAfter(onDate)) {
@@ -1056,7 +1062,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
feeCharges = feeCharges.plus(currentInstallment.getFeeChargesOutstanding(currency));
penaltyCharges = penaltyCharges.plus(currentInstallment.getPenaltyChargesOutstanding(currency));
} else {
outstandingPrincipal = outstandingPrincipal.plus(currentInstallment.getPrincipal(currency));
principalOutstanding = principalOutstanding.plus(currentInstallment.getPrincipal(currency));
totalPrincipal = totalPrincipal.minus(currentInstallment.getPrincipalCompleted(currency));
totalInterest = totalInterest.minus(currentInstallment.getInterestPaid(currency)).minus(
currentInstallment.getInterestWaived(currency));
@@ -1064,25 +1070,74 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
calculateInterestFrom = currentInstallment.getFromDate();
dueDate = currentInstallment.getDueDate();
inerestForCurrentInstallment = currentInstallment.getInterestCharged(currency);
if (currentInstallment.isRecalculatedInterestComponent()) {
isInterestRepayment = true;
} else {
isInterestRepayment = false;
}
repaymentDueDate = null;
periodNumber = currentInstallment.getInstallmentNumber();
}
if (isInterestRepayment && repaymentDueDate == null && !currentInstallment.isRecalculatedInterestComponent()) {
repaymentDueDate = currentInstallment.getDueDate();
}
}
}
}
if (applicationTerms.getInterestChargedFromLocalDate() != null
&& calculateInterestFrom.isBefore(applicationTerms.getInterestChargedFromLocalDate())) {
calculateInterestFrom = applicationTerms.getInterestChargedFromLocalDate();
if (interestChargedFromLocalDate != null && calculateInterestFrom.isBefore(interestChargedFromLocalDate)) {
calculateInterestFrom = interestChargedFromLocalDate;
}
Money interest = Money.zero(currency);
;
if (calculateInterestFrom.isBefore(onDate)) {
int daysInPeriodApplicableForInterest = Days.daysBetween(calculateInterestFrom, dueDate).getDays();
boolean useDailyInterest = false;
if (repaymentDueDate == null) {
if (isInterestRepayment) {
useDailyInterest = true;
}
repaymentDueDate = dueDate;
}
int daysInPeriodApplicableForInterest = Days.daysBetween(calculateInterestFrom, repaymentDueDate).getDays();
int days = Days.daysBetween(calculateInterestFrom, onDate).getDays();
interest = interest.plus(calculateInterestForDays(daysInPeriodApplicableForInterest, inerestForCurrentInstallment, days));
BigDecimal interestForInstallment = BigDecimal.ZERO;
if (useDailyInterest) {
interestForInstallment = loanApplicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc,
principalOutstanding, calculateInterestFrom, repaymentDueDate);
} else {
LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(
loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms, true);
final LocalDate idealDisbursementDate = this.scheduledDateGenerator
.idealDisbursementDateBasedOnFirstRepaymentDate(loanApplicationTerms.getLoanTermPeriodFrequencyType(),
loanApplicationTerms.getRepaymentEvery(), firstRepaymentdate);
LocalDate periodStartDateApplicableForInterest = calculateInterestFrom;
if (calculateInterestFrom.isBefore(idealDisbursementDate)) {
if (loanApplicationTerms.getInterestChargedFromLocalDate() != null) {
periodStartDateApplicableForInterest = loanApplicationTerms.getInterestChargedFromLocalDate();
} else {
periodStartDateApplicableForInterest = idealDisbursementDate;
}
daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDateApplicableForInterest, repaymentDueDate).getDays();
}
Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator
.calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, repaymentDueDate,
loanApplicationTerms.getInterestChargedFromLocalDate(),
loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery());
PrincipalInterest principalInterest = loanApplicationTerms.calculateTotalInterestForPeriod(
this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc,
totalOutstandingInterestPaymentDueToGrace.zero(), daysInPeriodApplicableForInterest, principalOutstanding);
interestForInstallment = principalInterest.interest().getAmount();
}
interest = interest.plus(inerestForCurrentInstallment.minus(interestForInstallment));
interest = interest.plus(calculateInterestForDays(daysInPeriodApplicableForInterest, interestForInstallment, days));
}
totalInterest = totalInterest.plus(interest);
totalPrincipal = totalPrincipal.plus(outstandingPrincipal);
totalPrincipal = totalPrincipal.plus(principalOutstanding);
return new LoanRepaymentScheduleInstallment(null, 0, onDate, onDate, totalPrincipal.getAmount(), totalInterest.getAmount(),
feeCharges.getAmount(), penaltyCharges.getAmount(), false);
@@ -1131,7 +1186,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
private LoanTransaction getPreclosureTransaction(List<LoanTransaction> loanTransactions,
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, final LoanApplicationTerms applicationTerms) {
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, final LoanApplicationTerms applicationTerms,
MathContext mc) {
LoanTransaction precloseTransaction = null;
for (LoanTransaction loanTransaction : loanTransactions) {
if (precloseTransaction == null || precloseTransaction.getTransactionDate().isBefore(loanTransaction.getTransactionDate())) {
@@ -1140,7 +1196,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
if (precloseTransaction != null) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = calculatePrepaymentAmount(installments, currency,
applicationTerms, precloseTransaction.getTransactionDate());
precloseTransaction.getTransactionDate(), applicationTerms.getInterestChargedFromLocalDate(), applicationTerms, mc);
if (precloseTransaction.getAmount(currency).isLessThan(loanRepaymentScheduleInstallment.getTotalOutstanding(currency))) {
precloseTransaction = null;
}
@@ -31,5 +31,5 @@ public interface LoanScheduleGenerator {
List<LoanRepaymentScheduleInstallment> previousSchedule, LocalDate recalculateFrom);
LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
LoanApplicationTerms applicationTerms, LocalDate onDate);
LocalDate onDate, LocalDate interestChargedFromLocalDate, LoanApplicationTerms loanApplicationTerms, MathContext mc);
}
@@ -29,6 +29,7 @@ import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException;
import org.mifosplatform.portfolio.calendar.exception.MeetingFrequencyMismatchException;
@@ -44,13 +45,18 @@ import org.mifosplatform.portfolio.loanaccount.api.LoanApiConstants;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler;
import org.mifosplatform.portfolio.loanproduct.LoanProductConstants;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
@@ -358,6 +364,49 @@ public class LoanScheduleAssembler {
workingDays);
}
public LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationTerms loanApplicationTerms, final Long officeId,
List<LoanTransaction> transactions, final Set<LoanCharge> loanCharges,
final List<LoanRepaymentScheduleInstallment> previousSchedule,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate recalculateFrom) {
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository
.findOneWithNotFoundDetection(loanApplicationTerms.getCurrency());
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, loanApplicationTerms
.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
return loanScheduleGenerator.rescheduleNextInstallments(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, transactions, loanRepaymentScheduleTransactionProcessor, previousSchedule,
recalculateFrom);
}
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms,final Long officeId) {
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, loanApplicationTerms
.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance();
LocalDate nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), onDate);
if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) {
nextScheduleDate = scheduledDateGenerator.adjustRepaymentDate(nextScheduleDate, loanApplicationTerms, isHolidayEnabled,
holidays, workingDays);
}
return loanScheduleGenerator.calculatePrepaymentAmount(installments, currency, nextScheduleDate,
loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms, mc);
}
private void validateDisbursementDateIsOnNonWorkingDay(final LocalDate disbursementDate, final WorkingDays workingDays) {
if (!WorkingDaysUtil.isWorkingDay(workingDays, disbursementDate)) {
final String errorMessage = "The expected disbursement date cannot be on a non working day";
@@ -6,9 +6,12 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import org.mifosplatform.infrastructure.core.api.JsonQuery;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
public interface LoanScheduleCalculationPlatformService {
LoanScheduleModel calculateLoanSchedule(JsonQuery query, Boolean validateParams);
void updateFutureSchedule(LoanScheduleData loanScheduleData, Long loanId);
}
@@ -6,17 +6,36 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonQuery;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.accountdetails.domain.AccountType;
import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.mifosplatform.portfolio.loanaccount.serialization.CalculateLoanScheduleQueryFromApiJsonHelper;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper;
import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler;
import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository;
@@ -35,12 +54,20 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanApplicationCommandFromApiJsonHelper loanApiJsonDeserializer;
private final LoanAssembler loanAssembler;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final LoanAccountDomainService accountDomainService;
private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
@Autowired
public LoanScheduleCalculationPlatformServiceImpl(final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer,
final LoanScheduleAssembler loanScheduleAssembler, final FromJsonHelper fromJsonHelper,
final LoanProductRepository loanProductRepository, final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer,
final LoanReadPlatformService loanReadPlatformService, final LoanApplicationCommandFromApiJsonHelper loanApiJsonDeserializer) {
final LoanReadPlatformService loanReadPlatformService, final LoanApplicationCommandFromApiJsonHelper loanApiJsonDeserializer,
final LoanAssembler loanAssembler, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
final LoanAccountDomainService accountDomainService, final CalendarInstanceRepository calendarInstanceRepository,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory) {
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanScheduleAssembler = loanScheduleAssembler;
this.fromJsonHelper = fromJsonHelper;
@@ -48,6 +75,11 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
this.loanProductCommandFromApiJsonDeserializer = loanProductCommandFromApiJsonDeserializer;
this.loanReadPlatformService = loanReadPlatformService;
this.loanApiJsonDeserializer = loanApiJsonDeserializer;
this.loanAssembler = loanAssembler;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.accountDomainService = accountDomainService;
this.calendarInstanceRepository = calendarInstanceRepository;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
}
@Override
@@ -83,4 +115,77 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson());
}
@Override
public void updateFutureSchedule(LoanScheduleData loanScheduleData, final Long loanId) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
if (!loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.isNpa() || !loan.status().isActive()
|| !loanRepaymentScheduleTransactionProcessor.isInterestFirstRepaymentScheduleTransactionProcessor()) { return; }
MonetaryCurrency currency = loan.getCurrency();
Money totalPrincipal = Money.zero(currency);
final List<LoanSchedulePeriodData> futureInstallments = new ArrayList<>();
for (final LoanRepaymentScheduleInstallment currentInstallment : loan.fetchRepaymentScheduleInstallments()) {
if (currentInstallment.isNotFullyPaidOff()) {
if (!currentInstallment.getDueDate().isAfter(LocalDate.now())) {
totalPrincipal = totalPrincipal.plus(currentInstallment.getPrincipalOutstanding(currency));
}
}
}
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final CalendarInstance restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(
loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
LocalDate calculatedRepaymentsStartingFromDate = accountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(applicationCurrency,
calculatedRepaymentsStartingFromDate, restCalendarInstance);
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = this.loanScheduleAssembler.calculatePrepaymentAmount(
loan.fetchRepaymentScheduleInstallments(), currency, LocalDate.now(), loanApplicationTerms, loan.getOfficeId());
Money totalAmount = totalPrincipal.plus(loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency)).plus(
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency));
Money interestDue = Money.zero(currency);
if (loanRepaymentScheduleInstallment.isInterestDue(currency)) {
interestDue = loanRepaymentScheduleInstallment.getInterestOutstanding(currency);
totalAmount = totalAmount.plus(interestDue);
}
boolean isNewPaymentRequired = loanRepaymentScheduleInstallment.isInterestDue(currency) || totalPrincipal.isGreaterThanZero();
final List<LoanTransaction> modifiedTransactions = new ArrayList<>();
List<LoanTransaction> transactions = loan.retreiveListOfTransactionsPostDisbursementExcludeAccruals();
for (LoanTransaction loanTransaction : transactions) {
modifiedTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
}
if (isNewPaymentRequired) {
LoanTransaction ondayPaymentTransaction = LoanTransaction.repayment(null, totalAmount, null, LocalDate.now(), null);
modifiedTransactions.add(ondayPaymentTransaction);
}
LoanScheduleModel model = this.loanScheduleAssembler.assembleForInterestRecalculation(loanApplicationTerms, loan.getOfficeId(),
modifiedTransactions, loan.charges(), loan.fetchRepaymentScheduleInstallments(), loanRepaymentScheduleTransactionProcessor,
LocalDate.now());
LoanScheduleData scheduleDate = model.toData();
Collection<LoanSchedulePeriodData> periodDatas = scheduleDate.getPeriods();
for (LoanSchedulePeriodData periodData : periodDatas) {
if ((periodData.periodDueDate().isEqual(LocalDate.now()) || periodData.periodDueDate().isAfter(LocalDate.now()))
&& isNewPaymentRequired) {
LoanSchedulePeriodData loanSchedulePeriodData = LoanSchedulePeriodData.repaymentOnlyPeriod(periodData.periodNumber(),
periodData.periodFromDate(), periodData.periodDueDate(), totalPrincipal.getAmount(), periodData
.principalLoanBalanceOutstanding(), interestDue.getAmount(), loanRepaymentScheduleInstallment
.getFeeChargesCharged(currency).getAmount(),
loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency).getAmount(), totalAmount.getAmount());
futureInstallments.add(loanSchedulePeriodData);
isNewPaymentRequired = false;
} else if (periodData.periodDueDate().isAfter(LocalDate.now())) {
futureInstallments.add(periodData);
}
}
loanScheduleData.updateFuturePeriods(futureInstallments);
}
}
@@ -614,8 +614,12 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
this.loanRepository.save(existingLoanApplication);
if (productRelatedDetail.isInterestRecalculationEnabled()) {
LocalDate recalculationFrequencyDate = existingLoanApplication.loanProduct().getProductInterestRecalculationDetails()
LocalDate recalculationFrequencyDate = existingLoanApplication.loanInterestRecalculationDetails()
.getRestFrequencyLocalDate();
if (recalculationFrequencyDate == null) {
recalculationFrequencyDate = existingLoanApplication.loanProduct().getProductInterestRecalculationDetails()
.getRestFrequencyLocalDate();
}
if (this.fromJsonHelper.parameterExists(LoanProductConstants.recalculationRestFrequencyDateParamName, command.parsedJson())) {
recalculationFrequencyDate = this.fromJsonHelper.extractLocalDateNamed(
LoanProductConstants.recalculationRestFrequencyDateParamName, command.parsedJson());
@@ -358,7 +358,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private void updateRecurringCalendarDatesForInterestRecalculation(final Loan loan) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& loan.loanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment() && !loan.isDisbursed()) {
final CalendarInstance calendarInstanceForInterestRecalculation = this.calendarInstanceRepository
.findByEntityIdAndEntityTypeIdAndCalendarTypeId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue(), CalendarType.COLLECTION.getValue());