diff --git a/mifosng-db/migrations/core_db/V197__updated_loan_running_balance_of_transactions.sql b/mifosng-db/migrations/core_db/V197__updated_loan_running_balance_of_transactions.sql new file mode 100755 index 000000000..4b9cdcfd4 --- /dev/null +++ b/mifosng-db/migrations/core_db/V197__updated_loan_running_balance_of_transactions.sql @@ -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`; + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index 9f545c247..9fd5ceff9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -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")) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 66be050e7..5503a487f 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -2095,6 +2095,7 @@ public class Loan extends AbstractPersistable { chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); chargesPayment.updateLoan(this); this.loanTransactions.add(chargesPayment); + updateLoanOutstandingBalaces(); } } @@ -2139,6 +2140,7 @@ public class Loan extends AbstractPersistable { 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 { return repaymentsOrWaivers; } - private List retreiveListOfTransactionsPostDisbursementExcludeAccruals() { + public List retreiveListOfTransactionsPostDisbursementExcludeAccruals() { final List 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 { final boolean isHolidayEnabled, final List 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 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 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 = new ArrayList<>(); @@ -4149,30 +4191,7 @@ public class Loan extends AbstractPersistable { 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 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 { 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 = new ArrayList<>(); @@ -4209,8 +4231,9 @@ public class Loan extends AbstractPersistable { 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 { } } + public LoanTransactionProcessingStrategy transactionProcessingStrategy() { + return this.transactionProcessingStrategy; + } + + public boolean isNpa() { + return this.isNpa; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java index d019a371e..d29685562 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java @@ -49,6 +49,8 @@ public class LoanScheduleData { */ private final Collection periods; + private Collection futurePeriods; + public LoanScheduleData(final CurrencyData currency, final Collection 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 getPeriods() { return this.periods; } + + public void updateFuturePeriods(Collection futurePeriods) { + this.futurePeriods = futurePeriods; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index 259099a86..9b060684d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -414,4 +414,8 @@ public class LoanSchedulePeriodData { public BigDecimal totalOverdue() { return defaultToZeroIfNull(this.totalOverdue); } + + public BigDecimal principalLoanBalanceOutstanding() { + return this.principalLoanBalanceOutstanding; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index f564e8a0e..ce0c66592 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -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 installments = previousSchedule; List 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 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 loanTransactions, - List installments, MonetaryCurrency currency, final LoanApplicationTerms applicationTerms) { + List 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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 9243db69e..7d819ad1b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -31,5 +31,5 @@ public interface LoanScheduleGenerator { List previousSchedule, LocalDate recalculateFrom); LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List installments, MonetaryCurrency currency, - LoanApplicationTerms applicationTerms, LocalDate onDate); + LocalDate onDate, LocalDate interestChargedFromLocalDate, LoanApplicationTerms loanApplicationTerms, MathContext mc); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 14c3081a7..4428ce0fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -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 transactions, final Set loanCharges, + final List 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 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 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 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"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java index ddb6a1357..d61ab58ad 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java @@ -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); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index 425748dbd..3b85035e0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -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 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 modifiedTransactions = new ArrayList<>(); + List 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 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); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 28be6003f..e7d7c561f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -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()); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 43380cced..3153e3d98 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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());