Merge pull request #1052 from pramodn02/develop

MIFOSX-1490 : added interest schedule for interest first repayment strat...
This commit is contained in:
Vishwas Babu A J
2014-08-28 23:06:16 -07:00
20 changed files with 678 additions and 241 deletions
@@ -0,0 +1,2 @@
ALTER TABLE `m_loan_repayment_schedule`
ADD COLUMN `recalculated_interest_component` TINYINT(1) NOT NULL DEFAULT '0';
@@ -8,6 +8,7 @@ package org.mifosplatform.integrationtests;
import static org.junit.Assert.assertEquals;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -15,6 +16,7 @@ import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.junit.Assert;
import org.junit.Before;
@@ -722,9 +724,28 @@ public class AccountingScenarioIntegrationTest {
e.printStackTrace();
}
final ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec,
loanID);
// MAKE 1
final float FIRST_INTEREST = 200.0f;
final float INTEREST_4_DAYS = 13.33F;
List fromDateList = (List) loanSchedule.get(1).get("fromDate");
LocalDate fromDateLocal = LocalDate.now();
fromDateLocal = fromDateLocal.withYear((int) fromDateList.get(0));
fromDateLocal = fromDateLocal.withMonthOfYear((int) fromDateList.get(1));
fromDateLocal = fromDateLocal.withDayOfMonth((int) fromDateList.get(2));
List dueDateList = (List) loanSchedule.get(1).get("dueDate");
LocalDate dueDateLocal = LocalDate.now();
dueDateLocal = dueDateLocal.withYear((int) dueDateList.get(0));
dueDateLocal = dueDateLocal.withMonthOfYear((int) dueDateList.get(1));
dueDateLocal = dueDateLocal.withDayOfMonth((int) dueDateList.get(2));
int totalDaysInPeriod = Days.daysBetween(fromDateLocal, dueDateLocal).getDays();
float totalInterest = (float) loanSchedule.get(1).get("interestOriginalDue");
DecimalFormat numberFormat = new DecimalFormat("#.00");
float INTEREST_4_DAYS = totalInterest / totalDaysInPeriod * 4;
INTEREST_4_DAYS = new Float(numberFormat.format(INTEREST_4_DAYS));
this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(currentDate), INTEREST_4_DAYS, FEE_PORTION,
PENALTY_PORTION, loanID);
@@ -797,8 +818,27 @@ public class AccountingScenarioIntegrationTest {
this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOndate);
final ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec,
loanID);
// MAKE 1
final float INTEREST_3_DAYS = 10.0F;
List fromDateList = (List) loanSchedule.get(1).get("fromDate");
LocalDate fromDateLocal = LocalDate.now();
fromDateLocal = fromDateLocal.withYear((int) fromDateList.get(0));
fromDateLocal = fromDateLocal.withMonthOfYear((int) fromDateList.get(1));
fromDateLocal = fromDateLocal.withDayOfMonth((int) fromDateList.get(2));
List dueDateList = (List) loanSchedule.get(1).get("dueDate");
LocalDate dueDateLocal = LocalDate.now();
dueDateLocal = dueDateLocal.withYear((int) dueDateList.get(0));
dueDateLocal = dueDateLocal.withMonthOfYear((int) dueDateList.get(1));
dueDateLocal = dueDateLocal.withDayOfMonth((int) dueDateList.get(2));
int totalDaysInPeriod = Days.daysBetween(fromDateLocal, dueDateLocal).getDays();
float totalInterest = (float) loanSchedule.get(1).get("interestOriginalDue");
DecimalFormat numberFormat = new DecimalFormat("#.00");
float INTEREST_3_DAYS = totalInterest / totalDaysInPeriod * 3;
INTEREST_3_DAYS = new Float(numberFormat.format(INTEREST_3_DAYS));
this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(runOndate), INTEREST_3_DAYS, FEE_PORTION,
PENALTY_PORTION, loanID);
@@ -977,7 +977,7 @@ public class Loan extends AbstractPersistable<Long> {
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue());
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
addRepaymentScheduleInstallment(installment);
}
}
@@ -1747,8 +1747,7 @@ public class Loan extends AbstractPersistable<Long> {
}
public ChangedTransactionDetail disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser,
final JsonCommand command, final ApplicationCurrency currency, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final Map<String, Object> actualChanges,
final JsonCommand command, final ApplicationCurrency currency, final Map<String, Object> actualChanges,
final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, final boolean allowTransactionsOnNonWorkingDay,
final boolean recalculateSchedule, final CalendarInstance calendarInstanceForInterestRecalculation) {
@@ -1758,9 +1757,6 @@ public class Loan extends AbstractPersistable<Long> {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
@@ -3774,15 +3770,17 @@ public class Loan extends AbstractPersistable<Long> {
final Set<LoanInstallmentCharge> loanChargePerInstallments = new HashSet<>();
if (loanCharge.isInstalmentFee()) {
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments()));
} else {
amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment)
.getAmount();
if (!installment.isRecalculatedInterestComponent()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments()));
} else {
amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(),
installment).getAmount();
}
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
loanChargePerInstallments.add(loanInstallmentCharge);
}
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
loanChargePerInstallments.add(loanInstallmentCharge);
}
}
return loanChargePerInstallments;
@@ -4140,8 +4138,8 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanSummaryDerivedFields();
}
public Money fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) {
Money amount = Money.zero(getCurrency());
public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) {
LoanRepaymentScheduleInstallment installment = null;
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
@@ -4177,21 +4175,27 @@ public class Loan extends AbstractPersistable<Long> {
loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData,
this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate());
amount = loanScheduleGenerator.fetchPrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(), loanApplicationTerms,
mc);
installment = loanScheduleGenerator.calculatePrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(),
loanApplicationTerms, LocalDate.now());
} else {
amount = this.getTotalOutstandingOnLoan();
installment = this.getTotalOutstandingOnLoan();
}
return amount;
return installment;
}
private Money getTotalOutstandingOnLoan() {
Money cumulativeOurstanding = Money.zero(loanCurrency());
private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() {
Money feeCharges = Money.zero(loanCurrency());
Money penaltyCharges = Money.zero(loanCurrency());
Money totalPrincipal = Money.zero(loanCurrency());
Money totalInterest = Money.zero(loanCurrency());
for (final LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeOurstanding = cumulativeOurstanding.plus(scheduledRepayment.getTotalOutstanding(getCurrency()));
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loanCurrency()));
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency()));
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency()));
}
return cumulativeOurstanding;
return new LoanRepaymentScheduleInstallment(null, 0, LocalDate.now(), LocalDate.now(), totalPrincipal.getAmount(),
totalInterest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount(), false);
}
public List<LoanRepaymentScheduleInstallment> fetchRepaymentScheduleInstallments() {
@@ -99,6 +99,9 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
@Column(name = "obligations_met_on_date")
private Date obligationsMetOnDate;
@Column(name = "recalculated_interest_component", nullable = false)
private boolean recalculatedInterestComponent;
protected LoanRepaymentScheduleInstallment() {
this.installmentNumber = null;
this.fromDate = null;
@@ -108,7 +111,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges) {
final BigDecimal penaltyCharges, boolean recalculatedInterestComponent) {
this.loan = loan;
this.installmentNumber = installmentNumber;
this.fromDate = fromDate.toDateTimeAtStartOfDay().toDate();
@@ -118,6 +121,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.feeChargesCharged = defaultToNullIfZero(feeCharges);
this.penaltyCharges = defaultToNullIfZero(penaltyCharges);
this.obligationsMet = false;
this.recalculatedInterestComponent = recalculatedInterestComponent;
}
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
@@ -540,4 +544,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
public Money getTotalPaidLate(final MonetaryCurrency currency) {
return Money.of(currency, this.totalPaidLate);
}
public boolean isRecalculatedInterestComponent() {
return this.recalculatedInterestComponent;
}
public void setRecalculatedInterestComponent(boolean recalculatedInterestComponent) {
this.recalculatedInterestComponent = recalculatedInterestComponent;
}
}
@@ -30,18 +30,18 @@ public class LoanRepaymentScheduleProcessingWrapper {
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges,
currency, period, repaymentPeriods.size(), totalInterest);
currency, period, repaymentPeriods.size(), totalInterest, !period.isRecalculatedInterestComponent());
final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges,
currency);
currency, !period.isRecalculatedInterestComponent());
final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(),
loanCharges, currency);
loanCharges, currency, !period.isRecalculatedInterestComponent());
final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(),
loanCharges, currency, period, repaymentPeriods.size(), totalInterest);
loanCharges, currency, period, repaymentPeriods.size(), totalInterest, !period.isRecalculatedInterestComponent());
final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(),
loanCharges, currency);
loanCharges, currency, !period.isRecalculatedInterestComponent());
final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate,
period.getDueDate(), loanCharges, currency);
period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent());
period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod,
feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod,
@@ -53,13 +53,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, int numberOfRepayments,
final Money totalInterest) {
final Money totalInterest, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -75,11 +75,11 @@ public class LoanRepaymentScheduleProcessingWrapper {
} else {
cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments)));
}
}else if (loanCharge.isOverdueInstallmentCharge()
} else if (loanCharge.isOverdueInstallmentCharge()
&& loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) {
cumulative = cumulative.plus(loanCharge.chargeAmount());
}else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
} else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -101,13 +101,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
}
private Money cumulativeFeeChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency) {
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency));
@@ -122,13 +122,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
}
private Money cumulativeFeeChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency) {
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency));
@@ -144,13 +144,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period,
int numberOfRepayments, Money totalInterest) {
int numberOfRepayments, Money totalInterest, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -166,7 +166,7 @@ public class LoanRepaymentScheduleProcessingWrapper {
} else {
cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments)));
}
}else if (loanCharge.isOverdueInstallmentCharge()
} else if (loanCharge.isOverdueInstallmentCharge()
&& loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) {
cumulative = cumulative.plus(loanCharge.chargeAmount());
@@ -192,13 +192,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
}
private Money cumulativePenaltyChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency) {
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency));
@@ -213,13 +213,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
}
private Money cumulativePenaltyChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency) {
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency));
@@ -6,7 +6,6 @@
package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -26,6 +25,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
/**
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor}
@@ -438,29 +438,51 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
@Override
public Map<LocalDate, Money> handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement,
public List<RecalculationDetail> handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement,
final MonetaryCurrency currency, final List<LoanRepaymentScheduleInstallment> installments,
final LoanRepaymentScheduleInstallment installment, final Map<LocalDate, LocalDate> recalculationDates) {
Map<LocalDate, Money> unprocessedMap = new HashMap<>();
final LoanRepaymentScheduleInstallment installment, final Map<LocalDate, LocalDate> recalculationDates,
final LoanTransaction preCloseTransaction) {
List<RecalculationDetail> earlypaymentDetail = new ArrayList<>();
Money currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null;
LocalDate transactionDate = loanTransaction.getTransactionDate();
if (loanTransaction != preCloseTransaction) {
transactionDate = recalculationDates.get(transactionDate);
}
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
newLoanTransaction.resetDerivedComponents();
Money unProcessed = processTransaction(newLoanTransaction, currency, installments, amountToProcess);
if (loanTransaction.getTransactionDate().isAfter(installment.getFromDate())
&& recalculationDates.get(loanTransaction.getTransactionDate()).isBefore(installment.getDueDate())) {
if (transactionDate.isAfter(installment.getFromDate()) && transactionDate.isBefore(installment.getDueDate())) {
Money earlyPayment = currentInstallmentOutstanding.minus(installment.getTotalOutstanding(currency));
if (earlyPayment.isGreaterThanZero()) {
unprocessedMap.put(loanTransaction.getTransactionDate(), earlyPayment);
earlypaymentDetail.add(new RecalculationDetail(false, transactionDate, null, earlyPayment, false));
}
currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
}
if (unProcessed.isGreaterThanZero()) {
unprocessedMap.put(loanTransaction.getTransactionDate(), unProcessed);
earlypaymentDetail.add(new RecalculationDetail(false, transactionDate, null, unProcessed, false));
}
}
return unprocessedMap;
return earlypaymentDetail;
}
@Override
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
return false;
}
@Override
public void applyTransaction(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) {
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null;
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
newLoanTransaction.resetDerivedComponents();
processTransaction(newLoanTransaction, currency, installments, amountToProcess);
}
}
}
@@ -11,11 +11,11 @@ import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
public interface LoanRepaymentScheduleTransactionProcessor {
@@ -28,8 +28,16 @@ public interface LoanRepaymentScheduleTransactionProcessor {
void handleWriteOff(LoanTransaction loanTransaction, MonetaryCurrency loanCurrency,
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
Map<LocalDate, Money> handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<RecalculationDetail> handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, LoanRepaymentScheduleInstallment currentInstallment,
Map<LocalDate, LocalDate> recalculationDates);
Map<LocalDate, LocalDate> recalculationDates, LoanTransaction preCloseTransaction);
/**
* Used in interest recalculation to introduce new interest only installment.
*/
boolean isInterestFirstRepaymentScheduleTransactionProcessor();
void applyTransaction(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments);
}
@@ -204,4 +204,9 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
@Override
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
return true;
}
}
@@ -97,6 +97,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
LocalDate periodStartDateApplicableForInterest = periodStartDate;
int periodNumber = 1;
int instalmentNumber = 1;
Money totalCumulativePrincipal = principalDisbursed.zero();
Money totalCumulativeInterest = principalDisbursed.zero();
Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero();
@@ -109,12 +110,43 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
Money reducePrincipal = totalCumulativePrincipal.zero();
Money fixedEmiAmount = totalCumulativePrincipal.zero();
int daysCalcForInstallmentNumber = 0;
LocalDate scheduleStartDateAsPerFrequency = periodStartDate;
while (!outstandingBalance.isZero()) {
isFirstRepayment = false;
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isFirstRepayment);
LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isHolidayEnabled, holidays, workingDays);
// to insert a new schedule between actual schedule to collect
// interest(till the transaction date) first on a payment
boolean recalculatedInterestComponent = false;
RecalculationDetail interestonlyPeriodDetail = null;
if (diffAmt != null) {
for (RecalculationDetail recalculationDetail : diffAmt) {
if (recalculationDetail.isInterestompound() && recalculationDetail.getStartDate().isEqual(periodStartDate)) {
interestonlyPeriodDetail = recalculationDetail;
break;
}
}
}
LocalDate scheduledDueDate = null;
LocalDate scheduledDueDateAsPerFrequency = null;
int daysInPeriodApplicableInFullInstallment = 0;
// will change the schedule dates as per the interest only Period
if (interestonlyPeriodDetail == null) {
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isFirstRepayment);
scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isHolidayEnabled, holidays, workingDays);
scheduledDueDateAsPerFrequency = scheduledDueDate;
} else {
recalculatedInterestComponent = true;
scheduledDueDate = interestonlyPeriodDetail.getToDate();
daysCalcForInstallmentNumber = periodNumber;
LocalDate actualRepayment = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate,
loanApplicationTerms, isFirstRepayment);
scheduledDueDateAsPerFrequency = this.scheduledDateGenerator.adjustRepaymentDate(actualRepayment, loanApplicationTerms,
isHolidayEnabled, holidays, workingDays);
}
final int daysInPeriod = Days.daysBetween(periodStartDate, scheduledDueDate).getDays();
if (loanApplicationTerms.isMultiDisburseLoan()) {
loanApplicationTerms.setFixedEmiAmountForPeriod(scheduledDueDate);
@@ -141,10 +173,27 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDateApplicableForInterest, scheduledDueDate).getDays();
}
final double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator
.calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, scheduledDueDate,
loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(),
loanApplicationTerms.getRepaymentEvery());
// this block identifies full payment period as per the
// schedule(excluding interest payments) for interest calculation
daysInPeriodApplicableInFullInstallment = daysInPeriodApplicableForInterest;
if (daysCalcForInstallmentNumber == periodNumber) {
periodStartDateApplicableForInterest = scheduleStartDateAsPerFrequency;
if (scheduleStartDateAsPerFrequency.isBefore(idealDisbursementDate)) {
if (loanApplicationTerms.getInterestChargedFromLocalDate() != null) {
periodStartDateApplicableForInterest = loanApplicationTerms.getInterestChargedFromLocalDate();
} else {
periodStartDateApplicableForInterest = idealDisbursementDate;
}
}
daysInPeriodApplicableInFullInstallment = Days.daysBetween(periodStartDateApplicableForInterest,
scheduledDueDateAsPerFrequency).getDays();
}
double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator
.calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest,
scheduledDueDateAsPerFrequency, loanApplicationTerms.getInterestChargedFromLocalDate(),
loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery());
Money balanceForcalculation = outstandingBalance;
// reduce principal is the early payment, will processed as per the
// reschedule strategy
@@ -177,10 +226,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
// 5 determine principal,interest of repayment period
final PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(
PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(
this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal,
totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace,
daysInPeriodApplicableForInterest, balanceForcalculation, loanApplicationTerms, periodNumber, mc);
daysInPeriodApplicableInFullInstallment, balanceForcalculation, loanApplicationTerms, periodNumber, mc);
if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1) {
@@ -190,17 +239,34 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
// update cumulative fields for principal & interest
Money interestForThisinstallment = principalInterestForThisPeriod.interest();
totalCumulativePrincipal = totalCumulativePrincipal.plus(principalInterestForThisPeriod.principal());
if (daysCalcForInstallmentNumber == periodNumber) {
interestForThisinstallment = interestForThisinstallment.zero().plus(
calculateInterestForDays(daysInPeriodApplicableInFullInstallment, interestForThisinstallment,
daysInPeriodApplicableForInterest));
}
Money principalForThisPeriod = principalDisbursed.zero();
Money outstandingForThePeriod = outstandingBalance;
// Exclude principal portion for interest only installments
if (!recalculatedInterestComponent) {
principalForThisPeriod = principalInterestForThisPeriod.principal();
if (daysCalcForInstallmentNumber == periodNumber && loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) {
principalForThisPeriod = principalForThisPeriod.plus(principalInterestForThisPeriod.interest()).minus(
interestForThisinstallment);
}
totalCumulativePrincipal = totalCumulativePrincipal.plus(principalForThisPeriod);
// 6. update outstandingLoanBlance using correct 'principalDue'
outstandingBalance = outstandingBalance.minus(principalForThisPeriod);
}
totalCumulativeInterest = totalCumulativeInterest.plus(interestForThisinstallment);
totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace();
// 6. update outstandingLoanBlance using correct 'principalDue'
outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal());
Money actualOutstandingbalance = outstandingBalance;
Money feeChargesForInstallment = principalDisbursed.zero();
Money penaltyChargesForInstallment = principalDisbursed.zero();
Money principalForThisPeriod = principalInterestForThisPeriod.principal();
if (principalForThisPeriod.isGreaterThan(reducePrincipal)) {
principalForThisPeriod = principalForThisPeriod.minus(reducePrincipal);
reducePrincipal = reducePrincipal.zero();
@@ -213,51 +279,87 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment);
}
Money extraPrincipal = reducePrincipal.zero();
// this block is to identify interest based on late/early payment
if (diffAmt != null && !diffAmt.isEmpty()) {
BigDecimal interestDueToLatePayment = BigDecimal.ZERO;
BigDecimal interestReducedDueToEarlyPayment = BigDecimal.ZERO;
boolean isperiodPrincipalReduced = false;
for (RecalculationDetail detail : diffAmt) {
// will increase the principal portion and reduces interest
// as per the number of days. and also identifies reduce
// principal for reschedule strategy
if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate)
&& !detail.getStartDate().isAfter(scheduledDueDate)) {
reducePrincipal = reducePrincipal.plus(detail.getAmount());
int diffDays = Days.daysBetween(detail.getStartDate(), scheduledDueDate).getDays();
if (diffDays > 0) {
reducePrincipal = reducePrincipal.minus(principalForThisPeriod);
if (!detail.isInterestompound()) {
// will increase the principal portion and reduces
// interest
// as per the number of days. and also identifies reduce
// principal for reschedule strategy
if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate)
&& !detail.getStartDate().isAfter(scheduledDueDate)) {
reducePrincipal = reducePrincipal.plus(detail.getAmount());
extraPrincipal = extraPrincipal.plus(detail.getAmount());
int diffDays = Days.daysBetween(detail.getStartDate(), scheduledDueDate).getDays();
if (diffDays > 0 && !isperiodPrincipalReduced) {
reducePrincipal = reducePrincipal.minus(principalForThisPeriod);
isperiodPrincipalReduced = true;
}
LocalDate startDate = detail.getStartDate();
if (loanApplicationTerms.getInterestChargedFromLocalDate() != null
&& startDate.isBefore(loanApplicationTerms.getInterestChargedFromLocalDate())) {
startDate = loanApplicationTerms.getInterestChargedFromLocalDate();
}
LocalDate endDate = scheduledDueDate;
if (startDate.isBefore(endDate)) {
Money amountForInterestCalculation = detail.getAmount();
Money balanceDiff = outstandingForThePeriod.minus(extraPrincipal);
if (balanceDiff.isLessThanZero()) {
amountForInterestCalculation = amountForInterestCalculation.plus(balanceDiff);
}
interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.add(calculateInterestForSpecificDays(
mc, loanApplicationTerms, periodNumber, totalOutstandingInterestPaymentDueToGrace,
daysInPeriodApplicableInFullInstallment, interestCalculationGraceOnRepaymentPeriodFraction,
amountForInterestCalculation, startDate, endDate));
}
}
if (reducePrincipal.isLessThanZero()) {
reducePrincipal = reducePrincipal.zero();
} else {
Money actualOutstanding = outstandingBalance.minus(reducePrincipal);
principalForThisPeriod = reducePrincipal.plus(principalForThisPeriod);
if (actualOutstanding.isLessThanZero()) {
principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding);
// calculates the interest for late payment and increase
// the interest for the installment
else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate)
&& periodStartDate.isBefore(LocalDate.now())) {
LocalDate fromDate = periodStartDate;
LocalDate toDate = scheduledDueDate;
if (!detail.getStartDate().isBefore(periodStartDate)) {
fromDate = detail.getStartDate();
}
if (!detail.getToDate().isAfter(scheduledDueDate)) {
toDate = detail.getToDate();
}
if (toDate.isAfter(LocalDate.now())) {
toDate = LocalDate.now();
}
if (fromDate.isAfter(LocalDate.now())) {
fromDate = LocalDate.now();
}
if (loanApplicationTerms.getInterestChargedFromLocalDate() != null
&& fromDate.isBefore(loanApplicationTerms.getInterestChargedFromLocalDate())) {
fromDate = loanApplicationTerms.getInterestChargedFromLocalDate();
}
if (fromDate.isBefore(toDate)) {
interestDueToLatePayment = interestDueToLatePayment.add(calculateInterestForSpecificDays(mc,
loanApplicationTerms, periodNumber, totalOutstandingInterestPaymentDueToGrace,
daysInPeriodApplicableInFullInstallment, interestCalculationGraceOnRepaymentPeriodFraction,
detail.getAmount(), fromDate, toDate));
}
}
interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.add(loanApplicationTerms.interestRateFor(
this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), detail.getStartDate(), scheduledDueDate));
}
// calculates the interest for late payment and increase
// the interest for the installment
else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate)
&& periodStartDate.isBefore(LocalDate.now())) {
LocalDate fromDate = periodStartDate;
LocalDate toDate = scheduledDueDate;
if (!detail.getStartDate().isBefore(periodStartDate)) {
fromDate = detail.getStartDate();
}
if (!detail.getToDate().isAfter(scheduledDueDate)) {
toDate = detail.getToDate();
}
if (toDate.isAfter(LocalDate.now())) {
toDate = LocalDate.now();
}
interestDueToLatePayment = interestDueToLatePayment.add(loanApplicationTerms.interestRateFor(
this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), fromDate, toDate));
}
if (reducePrincipal.isLessThanZero()) {
reducePrincipal = reducePrincipal.zero();
} else {
Money actualOutstanding = outstandingBalance.minus(reducePrincipal);
principalForThisPeriod = reducePrincipal.plus(principalForThisPeriod);
if (actualOutstanding.isLessThanZero()) {
principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding);
}
}
@@ -281,9 +383,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
.plus(penaltyChargesForInstallment);
// 9. create repayment period from parts
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, periodStartDate,
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, periodStartDate,
scheduledDueDate, principalForThisPeriod, actualOutstandingbalance, interestForThisinstallment,
feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue);
feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue, recalculatedInterestComponent);
periods.add(installment);
// handle cumulative fields
@@ -296,7 +398,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
periodStartDate = scheduledDueDate;
periodStartDateApplicableForInterest = periodStartDate;
periodNumber++;
if (!recalculatedInterestComponent) {
periodNumber++;
scheduleStartDateAsPerFrequency = periodStartDate;
}
instalmentNumber++;
}
if (principalDisbursed.isNotEqualTo(expectedPrincipalDisburse)) {
@@ -311,10 +417,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Money.of(currency, loanScheduleModelPeriod.interestDue()), null);
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed,
totalCumulativeInterest, numberOfRepayments);
totalCumulativeInterest, numberOfRepayments, !loanScheduleModelPeriod.isRecalculatedInterestComponent());
Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed,
totalCumulativeInterest, numberOfRepayments);
totalCumulativeInterest, numberOfRepayments, !loanScheduleModelPeriod.isRecalculatedInterestComponent());
totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount());
totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount());
loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount());
@@ -324,14 +430,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// this block is to add extra re-payment schedules with interest portion
// if the last payment is missed
if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) {
boolean recalculatedInterestComponent = true;
Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>();
for (RecalculationDetail detail : diffAmt) {
if (!periodStartDate.isAfter(detail.getStartDate()) && detail.isLatePayment()) {
if (!periodStartDate.isAfter(detail.getStartDate()) && detail.isLatePayment() && !detail.isInterestompound()) {
if (processDetails.containsKey(detail.getToDate())) {
RecalculationDetail recalculationDetail = processDetails.get(detail.getToDate());
RecalculationDetail updatedDetail = new RecalculationDetail(recalculationDetail.isLatePayment(),
recalculationDetail.getStartDate(), recalculationDetail.getToDate(), recalculationDetail.getAmount().plus(
detail.getAmount()));
detail.getAmount()), detail.isInterestompound());
processDetails.put(updatedDetail.getToDate(), updatedDetail);
} else {
processDetails.put(detail.getToDate(), detail);
@@ -350,11 +457,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
totalInterestCharged = totalInterestCharged.add(interestDueToLatePayment.getAmount());
totalRepaymentExpected = totalRepaymentExpected.add(interestDueToLatePayment.getAmount());
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate,
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, fromDate,
toDate, interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment,
interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment);
interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment,
recalculatedInterestComponent);
periods.add(installment);
periodNumber++;
instalmentNumber++;
}
}
}
@@ -365,6 +473,23 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
totalOutstanding);
}
private BigDecimal calculateInterestForSpecificDays(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
int periodNumber, Money totalOutstandingInterestPaymentDueToGrace, int daysInPeriodApplicableForInterest,
double interestCalculationGraceOnRepaymentPeriodFraction, Money amount, LocalDate startDate, LocalDate endDate) {
PrincipalInterest principalInterest = loanApplicationTerms.calculateTotalInterestForPeriod(this.paymentPeriodsInOneYearCalculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, totalOutstandingInterestPaymentDueToGrace.zero(),
daysInPeriodApplicableForInterest, amount);
int days = Days.daysBetween(startDate, endDate).getDays();
double interest = calculateInterestForDays(daysInPeriodApplicableForInterest, principalInterest.interest(), days);
return BigDecimal.valueOf(interest);
}
private double calculateInterestForDays(int daysInPeriodApplicableForInterest, Money interest, int days) {
return ((interest.getAmount().doubleValue()) / daysInPeriodApplicableForInterest) * days;
}
public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator,
double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest,
Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, int daysInPeriodApplicableForInterest,
@@ -432,13 +557,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod,
final Money principalDisbursed, final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments) {
final Money principalDisbursed, final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments,
boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -482,13 +608,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency monetaryCurrency,
final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments) {
final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) {
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -544,9 +670,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
LoanScheduleModel loanScheduleModel = null;
List<LoanRepaymentScheduleInstallment> installments = previousSchedule;
List<LoanRepaymentScheduleInstallment> removeInstallments = new ArrayList<>();
MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency();
LoanTransaction preCloseTransaction = getPreclosureTransaction(transactions, installments, currency, loanApplicationTerms);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (!loanApplicationTerms.isPrincipalGraceApplicableForThisPeriod(installment.getInstallmentNumber())
&& installment.getPrincipal(loanApplicationTerms.getCurrency()).isZero()) {
if (installment.isRecalculatedInterestComponent() && recalculateFrom != null
&& !recalculateFrom.isAfter(installment.getDueDate())) {
removeInstallments.add(installment);
}
}
@@ -556,22 +684,129 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
workingDays);
installments = retrieveRepaymentSchedule(loanScheduleModel);
}
if (recalculateFrom == null) {
recalculateFrom = loanApplicationTerms.getExpectedDisbursementDate();
}
final List<RecalculationDetail> recalculationDetails = new ArrayList<>();
final Map<LocalDate, RecalculationDetail> retainRecalculationDetails = new HashMap<>();
LocalDate processTransactionsForInterestCompound = loanApplicationTerms.getExpectedDisbursementDate();
while (recalculateFrom.isAfter(processTransactionsForInterestCompound) && !transactions.isEmpty()
&& loanRepaymentScheduleTransactionProcessor.isInterestFirstRepaymentScheduleTransactionProcessor()) {
int installmentNumber = findLastProcessedInstallmentNumber(installments, processTransactionsForInterestCompound);
List<LoanTransaction> processTransactions = processTransactions(transactions, processTransactionsForInterestCompound);
if (processTransactions.size() > 0) {
loanScheduleModel = createInterestOnlyRecalculationDetails(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule,
loanScheduleModel, installments, currency, recalculationDetails, retainRecalculationDetails, false,
installmentNumber, processTransactions, preCloseTransaction);
}
recalculationDetails.retainAll(retainRecalculationDetails.values());
processTransactionsForInterestCompound = getNextRecalculateFromDate(transactions, processTransactionsForInterestCompound);
}
while (!recalculateFrom.isAfter(LocalDate.now())) {
int installmentNumber = findLastProcessedInstallmentNumber(installments, recalculateFrom);
List<LoanTransaction> processTransactions = processTransactions(transactions, recalculateFrom);
if (loanRepaymentScheduleTransactionProcessor.isInterestFirstRepaymentScheduleTransactionProcessor()
&& processTransactions.size() > 0) {
loanScheduleModel = createInterestOnlyRecalculationDetails(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule,
loanScheduleModel, installments, currency, recalculationDetails, retainRecalculationDetails, true,
installmentNumber, processTransactions, preCloseTransaction);
if (loanScheduleModel != null) {
installments = retrieveRepaymentSchedule(loanScheduleModel);
}
}
recalculationDetails.retainAll(retainRecalculationDetails.values());
loanScheduleModel = recalculateInstallment(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled,
holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule, loanScheduleModel, installments,
processTransactions, installmentNumber);
processTransactions, installmentNumber, recalculationDetails, preCloseTransaction);
if (loanScheduleModel != null) {
installments = retrieveRepaymentSchedule(loanScheduleModel);
}
recalculateFrom = getNextRecalculateFromDate(transactions, recalculateFrom);
}
return loanScheduleModel;
}
/**
* Method identifies interest only repayment periods
*/
private LoanScheduleModel createInterestOnlyRecalculationDetails(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> previousSchedule, LoanScheduleModel loanScheduleModel,
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
final List<RecalculationDetail> recalculationDetails, final Map<LocalDate, RecalculationDetail> retainRecalculationDetails,
boolean recalculate, int installmentNumber, List<LoanTransaction> processTransactions, LoanTransaction preCloseTransaction) {
LoanTransaction loanTransaction = processTransactions.remove(processTransactions.size() - 1);
if (loanTransaction == preCloseTransaction) {
processTransactions.add(loanTransaction);
return loanScheduleModel;
}
LocalDate processTransactionsForInterestCompound = getNextRestScheduleDate(loanTransaction.getTransactionDate().minusDays(1),
loanApplicationTerms, isHolidayEnabled, holidays, workingDays);
List<LoanRepaymentScheduleInstallment> scheduleInstallments = getInstallmentsForInterestCompound(installments,
processTransactionsForInterestCompound, retainRecalculationDetails.keySet());
if (!scheduleInstallments.isEmpty()) {
for (LoanRepaymentScheduleInstallment installment : installments) {
installment.resetDerivedComponents();
installment.updateDerivedFields(currency, loanApplicationTerms.getExpectedDisbursementDate());
}
loanRepaymentScheduleTransactionProcessor.applyTransaction(processTransactions, currency, installments);
Money principalUnprocessed = Money.zero(currency);
Money interestUnprocessed = Money.zero(currency);
Money feeUnprocessed = Money.zero(currency);
boolean isBeforeFirstInstallment = false;
if (scheduleInstallments.size() == 1) {
LoanRepaymentScheduleInstallment installment = scheduleInstallments.get(0);
if (installment.getInstallmentNumber() == 1
&& (installment.getDueDate().isAfter(loanTransaction.getTransactionDate()) || (installment
.isRecalculatedInterestComponent() && installment.getDueDate()
.isEqual(loanTransaction.getTransactionDate())))) {
isBeforeFirstInstallment = true;
}
}
if (!isBeforeFirstInstallment) {
for (LoanRepaymentScheduleInstallment installment : scheduleInstallments) {
principalUnprocessed = principalUnprocessed.plus(installment.getPrincipalOutstanding(currency));
interestUnprocessed = interestUnprocessed.plus(installment.getInterestOutstanding(currency));
feeUnprocessed = feeUnprocessed.plus(installment.getFeeChargesOutstanding(currency));
feeUnprocessed = feeUnprocessed.plus(installment.getPenaltyChargesOutstanding(currency));
}
}
if (interestUnprocessed.isLessThan(loanTransaction.getAmount(currency))) {
LoanRepaymentScheduleInstallment lastProcessedInstallment = scheduleInstallments.get(scheduleInstallments.size() - 1);
LocalDate startDate = lastProcessedInstallment.getDueDate();
if (isBeforeFirstInstallment) {
startDate = loanApplicationTerms.getExpectedDisbursementDate();
}
RecalculationDetail recalculationDetail = new RecalculationDetail(false, startDate, processTransactionsForInterestCompound,
null, true);
retainRecalculationDetails.put(processTransactionsForInterestCompound, recalculationDetail);
recalculationDetails.add(recalculationDetail);
if (recalculate) {
recalculationDetails.retainAll(retainRecalculationDetails.values());
loanScheduleModel = recalculateInstallment(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule,
loanScheduleModel, installments, processTransactions, installmentNumber, recalculationDetails,
preCloseTransaction);
}
}
}
processTransactions.add(loanTransaction);
return loanScheduleModel;
}
private LocalDate getNextRecalculateFromDate(List<LoanTransaction> processTransactions, LocalDate preCalculationDate) {
LocalDate recalculateFrom = null;
for (LoanTransaction loanTransaction : processTransactions) {
@@ -590,25 +825,28 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
* Method calls regenerate schedule for a particular set of transactions
* till all the installments are processed to identify early or late
* payments and will be used in regeneration of schedule
*
* @param preCloseTransaction
* TODO
*/
private LoanScheduleModel recalculateInstallment(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> previousSchedule, LoanScheduleModel loanScheduleModel,
List<LoanRepaymentScheduleInstallment> installments, List<LoanTransaction> processTransactions, final int skipRecalculation) {
List<LoanRepaymentScheduleInstallment> installments, List<LoanTransaction> processTransactions, final int skipRecalculation,
final List<RecalculationDetail> recalculationDetails, LoanTransaction preCloseTransaction) {
int processInstallmentsFrom = 0;
Integer numberOfInstallments = previousSchedule.size() - 1;
final List<RecalculationDetail> recalculationDetails = new ArrayList<>();
while (processInstallmentsFrom < numberOfInstallments) {
Integer lastInstallmentNumber = previousSchedule.size();
while (processInstallmentsFrom < lastInstallmentNumber) {
RecalculatedSchedule recalculatedSchedule = recalculateInterest(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, processTransactions, loanRepaymentScheduleTransactionProcessor,
processInstallmentsFrom, installments, recalculationDetails, skipRecalculation);
processInstallmentsFrom, installments, recalculationDetails, skipRecalculation, preCloseTransaction);
processInstallmentsFrom = recalculatedSchedule.getInstallmentNumber();
if (recalculatedSchedule.getLoanScheduleModel() != null) {
loanScheduleModel = recalculatedSchedule.getLoanScheduleModel();
installments = retrieveRepaymentSchedule(loanScheduleModel);
numberOfInstallments = installments.size() - 1;
lastInstallmentNumber = installments.size() - 1;
}
}
return loanScheduleModel;
@@ -616,13 +854,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
/**
* Method identifies late or early payments for schedule recalculation.
*
* @param preCloseTransaction
* TODO
*/
private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final int installmentNumber,
final List<LoanRepaymentScheduleInstallment> installments, final List<RecalculationDetail> recalculationDetails,
final int skipRecalculation) {
final int skipRecalculation, LoanTransaction preCloseTransaction) {
boolean processRecalculate = false;
int processedInstallmentNumber = installmentNumber;
@@ -642,9 +883,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
continue;
}
processedInstallmentNumber = installment.getInstallmentNumber();
if (installment.getDueDate().isAfter(LocalDate.now())) {
continue;
if (installment.getInstallmentNumber() == installments.size()) {
processRecalculate = true;
}
List<LoanTransaction> transactionsForInstallment = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
LocalDate transactionsDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms,
@@ -659,19 +901,18 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
workingDays));
}
}
Map<LocalDate, Money> earlyPaymentMap = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(
transactionsForInstallment, currency, processinstallmets, installment, recalculationDates);
if (installment.isRecalculatedInterestComponent() && installment.getPrincipal(currency).isGreaterThanZero()) {
diffAmt.add(new RecalculationDetail(false, installment.getDueDate(), null, installment.getPrincipal(currency), false));
}
List<RecalculationDetail> earlypaymentDetail = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(
transactionsForInstallment, currency, processinstallmets, installment, recalculationDates, preCloseTransaction);
// this block is to create early payment entries for schedule
// generation
for (Map.Entry<LocalDate, Money> entry : earlyPaymentMap.entrySet()) {
LocalDate startDate = entry.getKey();
if (unpaidPricipal.isGreaterThan(entry.getValue())) {
startDate = getNextRestScheduleDate(startDate.minusDays(1), loanApplicationTerms, isHolidayEnabled, holidays,
workingDays);
}
RecalculationDetail recalculationDetail = new RecalculationDetail(false, startDate, null, entry.getValue());
if (!entry.getKey().isAfter(installment.getDueDate()) && entry.getKey().isAfter(installment.getFromDate())) {
for (RecalculationDetail recalculationDetail : earlypaymentDetail) {
if (!recalculationDetail.getStartDate().isAfter(installment.getDueDate())
&& recalculationDetail.getStartDate().isAfter(installment.getFromDate())) {
diffAmt.add(recalculationDetail);
}
}
@@ -692,7 +933,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
startDate = startDate.minusDays(1);
}
applyRest(transactions, startDate, recalculateFrom, installment, loanRepaymentScheduleTransactionProcessor, currency,
loanApplicationTerms, isHolidayEnabled, holidays, workingDays, installments);
loanApplicationTerms, isHolidayEnabled, holidays, workingDays, installments, preCloseTransaction);
totalOutstanding = installment.getTotalOutstanding(currency);
if (totalOutstanding.isGreaterThanZero()) {
Money latepaymentoutstanding = installment.getPrincipalOutstanding(currency);
@@ -714,7 +955,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
break;
}
RecalculationDetail recalculationDetail = new RecalculationDetail(true, recalculateFrom, recalcualteTill,
latepaymentoutstanding);
latepaymentoutstanding, false);
diffAmt.add(recalculationDetail);
}
startDate = recalculateFrom.plusDays(1);
@@ -722,8 +963,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
}
if ((!diffAmt.isEmpty() && skipRecalculation <= installment.getInstallmentNumber())
|| installment.getInstallmentNumber() == installments.size() - 1) {
if (!diffAmt.isEmpty() && skipRecalculation <= installment.getInstallmentNumber()) {
processRecalculate = true;
}
break;
@@ -746,7 +986,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue());
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
installments.add(installment);
}
}
@@ -756,12 +996,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
/**
* Method to identify which transaction did the payment for current
* installment
*
* @param preCloseTransaction
* TODO
*/
private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to,
LoanRepaymentScheduleInstallment installment,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency,
final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays, final List<LoanRepaymentScheduleInstallment> installments) {
final WorkingDays workingDays, final List<LoanRepaymentScheduleInstallment> installments, LoanTransaction preCloseTransaction) {
List<LoanTransaction> transactions = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
for (LoanTransaction transaction : loanTransactions) {
@@ -776,7 +1019,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(transactions, currency, installments, installment,
recalculationDates);
recalculationDates, preCloseTransaction);
}
@SuppressWarnings("unused")
@@ -796,29 +1039,54 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
* Method returns the amount payable to close the loan account as of today.
*/
@Override
public Money fetchPrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
final LoanApplicationTerms applicationTerms, final MathContext mc) {
Money prepaymentAmount = Money.zero(currency);
Money amount = Money.zero(currency);
LocalDate calculateInterestFrom = LocalDate.now();
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, final LoanApplicationTerms applicationTerms, final LocalDate onDate) {
Money outstandingPrincipal = Money.zero(currency);
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;
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyPaidOff()) {
if (!currentInstallment.getDueDate().isAfter(LocalDate.now())) {
amount = amount.plus(currentInstallment.getTotalOutstanding(currency));
if (!currentInstallment.getDueDate().isAfter(onDate)) {
totalPrincipal = totalPrincipal.plus(currentInstallment.getPrincipalOutstanding(currency));
totalInterest = totalInterest.plus(currentInstallment.getInterestOutstanding(currency));
feeCharges = feeCharges.plus(currentInstallment.getFeeChargesOutstanding(currency));
penaltyCharges = penaltyCharges.plus(currentInstallment.getPenaltyChargesOutstanding(currency));
} else {
prepaymentAmount = prepaymentAmount.plus(currentInstallment.getPrincipalOutstanding(currency));
outstandingPrincipal = outstandingPrincipal.plus(currentInstallment.getPrincipal(currency));
totalPrincipal = totalPrincipal.minus(currentInstallment.getPrincipalCompleted(currency));
totalInterest = totalInterest.minus(currentInstallment.getInterestPaid(currency)).minus(
currentInstallment.getInterestWaived(currency));
if (currentInstallment.getFromDate().isBefore(calculateInterestFrom)) {
calculateInterestFrom = currentInstallment.getFromDate();
dueDate = currentInstallment.getDueDate();
inerestForCurrentInstallment = currentInstallment.getInterestCharged(currency);
}
}
}
}
if (applicationTerms.getInterestChargedFromLocalDate() != null
&& calculateInterestFrom.isBefore(applicationTerms.getInterestChargedFromLocalDate())) {
calculateInterestFrom = applicationTerms.getInterestChargedFromLocalDate();
}
Money interest = Money.zero(currency);
;
if (calculateInterestFrom.isBefore(onDate)) {
int daysInPeriodApplicableForInterest = Days.daysBetween(calculateInterestFrom, dueDate).getDays();
int days = Days.daysBetween(calculateInterestFrom, onDate).getDays();
interest = interest.plus(calculateInterestForDays(daysInPeriodApplicableForInterest, inerestForCurrentInstallment, days));
}
BigDecimal interest = applicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, prepaymentAmount,
calculateInterestFrom, LocalDate.now());
prepaymentAmount = prepaymentAmount.plus(interest).plus(amount);
return prepaymentAmount;
totalInterest = totalInterest.plus(interest);
totalPrincipal = totalPrincipal.plus(outstandingPrincipal);
return new LoanRepaymentScheduleInstallment(null, 0, onDate, onDate, totalPrincipal.getAmount(), totalInterest.getAmount(),
feeCharges.getAmount(), penaltyCharges.getAmount(), false);
}
private Integer findLastProcessedInstallmentNumber(final List<LoanRepaymentScheduleInstallment> installments, LocalDate date) {
@@ -832,6 +1100,27 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return installmentNumber;
}
private List<LoanRepaymentScheduleInstallment> getInstallmentsForInterestCompound(
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate date, Collection<LocalDate> processedDates) {
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = new ArrayList<>();
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isEqual(date)
&& (!installment.isRecalculatedInterestComponent() || (processedDates.contains(date) && installment
.isRecalculatedInterestComponent()))) {
repaymentScheduleInstallments.clear();
break;
} else if (installment.getDueDate().isBefore(date)) {
repaymentScheduleInstallments.add(installment);
} else {
if (installment.getInstallmentNumber() == 1) {
repaymentScheduleInstallments.add(installment);
}
break;
}
}
return repaymentScheduleInstallments;
}
private List<LoanTransaction> processTransactions(final List<LoanTransaction> transactions, final LocalDate tillDate) {
List<LoanTransaction> toProcess = new ArrayList<>();
for (LoanTransaction loanTransaction : transactions) {
@@ -842,4 +1131,24 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return toProcess;
}
private LoanTransaction getPreclosureTransaction(List<LoanTransaction> loanTransactions,
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, final LoanApplicationTerms applicationTerms) {
LoanTransaction precloseTransaction = null;
for (LoanTransaction loanTransaction : loanTransactions) {
if (precloseTransaction == null || precloseTransaction.getTransactionDate().isBefore(loanTransaction.getTransactionDate())) {
precloseTransaction = loanTransaction;
}
}
if (precloseTransaction != null) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = calculatePrepaymentAmount(installments, currency,
applicationTerms, precloseTransaction.getTransactionDate());
if (precloseTransaction.getAmount(currency).isLessThan(loanRepaymentScheduleInstallment.getTotalOutstanding(currency))) {
precloseTransaction = null;
}
}
return precloseTransaction;
}
}
@@ -236,6 +236,23 @@ public final class LoanApplicationTerms {
interestRecalculationCompoundingMethod, restCalendarInstance);
}
public static LoanApplicationTerms assembleFrom(final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType,
final LoanApplicationTerms applicationTerms) {
return new LoanApplicationTerms(applicationTerms.currency, applicationTerms.loanTermFrequency,
applicationTerms.loanTermPeriodFrequencyType, applicationTerms.numberOfRepayments, repaymentEvery,
repaymentPeriodFrequencyType, applicationTerms.amortizationMethod, applicationTerms.interestMethod,
applicationTerms.interestRatePerPeriod, applicationTerms.interestRatePeriodFrequencyType,
applicationTerms.annualNominalInterestRate, applicationTerms.interestCalculationPeriodMethod, applicationTerms.principal,
applicationTerms.expectedDisbursementDate, applicationTerms.repaymentsStartingFromDate,
applicationTerms.calculatedRepaymentsStartingFromDate, applicationTerms.principalGrace,
applicationTerms.interestPaymentGrace, applicationTerms.interestChargingGrace, applicationTerms.interestChargedFromDate,
applicationTerms.inArrearsTolerance, applicationTerms.multiDisburseLoan, applicationTerms.fixedEmiAmount,
applicationTerms.disbursementDatas, applicationTerms.maxOutstandingBalance, applicationTerms.emiAmountVariations,
applicationTerms.graceOnArrearsAgeing, applicationTerms.daysInMonthType, applicationTerms.daysInYearType,
applicationTerms.interestRecalculationEnabled, applicationTerms.rescheduleStrategyMethod,
applicationTerms.interestRecalculationCompoundingMethod, applicationTerms.restCalendarInstance);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod,
@@ -13,7 +13,6 @@ import org.joda.time.LocalDate;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -31,6 +30,6 @@ public interface LoanScheduleGenerator {
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
List<LoanRepaymentScheduleInstallment> previousSchedule, LocalDate recalculateFrom);
Money fetchPrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
LoanApplicationTerms applicationTerms, MathContext mc);
LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
LoanApplicationTerms applicationTerms, LocalDate onDate);
}
@@ -94,4 +94,9 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo
public void addLoanCharges(@SuppressWarnings("unused") BigDecimal feeCharge, @SuppressWarnings("unused") BigDecimal penaltyCharge) {
return;
}
@Override
public boolean isRecalculatedInterestComponent() {
return false;
}
}
@@ -31,4 +31,6 @@ public interface LoanScheduleModelPeriod {
BigDecimal penaltyChargesDue();
void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge);
boolean isRecalculatedInterestComponent();
}
@@ -26,18 +26,19 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
private Money feeChargesDue;
private Money penaltyChargesDue;
private Money totalDue;
private final boolean recalculatedInterestComponent;
public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate,
final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue,
final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue) {
final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue, boolean recalculatedInterestComponent) {
return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance,
interestDue, feeChargesDue, penaltyChargesDue, totalDue);
interestDue, feeChargesDue, penaltyChargesDue, totalDue, recalculatedInterestComponent);
}
public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate,
final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue,
final Money penaltyChargesDue, final Money totalDue) {
final Money penaltyChargesDue, final Money totalDue, final boolean recalculatedInterestComponent) {
this.periodNumber = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -47,6 +48,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
this.feeChargesDue = feeChargesDue;
this.penaltyChargesDue = penaltyChargesDue;
this.totalDue = totalDue;
this.recalculatedInterestComponent = recalculatedInterestComponent;
}
@Override
@@ -122,4 +124,9 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge);
this.totalDue = this.totalDue.plus(feeCharge).plus(penaltyCharge);
}
@Override
public boolean isRecalculatedInterestComponent() {
return this.recalculatedInterestComponent;
}
}
@@ -11,15 +11,18 @@ import org.mifosplatform.organisation.monetary.domain.Money;
public class RecalculationDetail {
private boolean isLatePayment;
private boolean isInterestompound;
private LocalDate startDate;
private LocalDate toDate;
private Money amount;
public RecalculationDetail(final boolean isLatePayment, final LocalDate startDate, final LocalDate toDate, final Money amount) {
public RecalculationDetail(final boolean isLatePayment, final LocalDate startDate, final LocalDate toDate, final Money amount,
final boolean isInterestompound) {
this.isLatePayment = isLatePayment;
this.startDate = startDate;
this.toDate = toDate;
this.amount = amount;
this.isInterestompound = isInterestompound;
}
public boolean isLatePayment() {
@@ -42,8 +45,11 @@ public class RecalculationDetail {
return !this.startDate.isAfter(endDate) && !startDate.isAfter(this.toDate);
}
public void updateAmount(Money amount) {
this.amount = amount;
}
public boolean isInterestompound() {
return this.isInterestompound;
}
}
@@ -24,8 +24,6 @@ import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.portfolio.common.domain.DaysInMonthType;
import org.mifosplatform.portfolio.common.domain.DaysInYearType;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
@@ -108,7 +106,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (accruredTill == null || lastLoanId == null || !lastLoanId.equals(accrualData.getLoanId())) {
accruredTill = accrualData.getAccruedTill();
}
addAccrualTillSpecificDate(tilldate, accrualData, accruredTill);
if (accruredTill == null || accruredTill.isBefore(tilldate)) {
addAccrualTillSpecificDate(tilldate, accrualData, accruredTill);
}
} else {
addAccrualAccounting(accrualData);
accruredTill = accrualData.getDueDateAsLocaldate();
@@ -131,10 +131,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData,
final LocalDate accruredTill) throws Exception {
LocalDate interestStartDate = accrualData.getFromDateAsLocaldate();
boolean isInterestStartDateEqulsInstallmentStartdate = true;
// boolean isInterestStartDateEqulsInstallmentStartdate = true;
if (accrualData.getInterestCalculatedFrom() != null
&& accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) {
isInterestStartDateEqulsInstallmentStartdate = false;
// isInterestStartDateEqulsInstallmentStartdate = false;
if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) {
interestStartDate = accrualData.getInterestCalculatedFrom();
} else {
@@ -157,22 +157,18 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
int daysToBeAccrued = Days.daysBetween(startDate, tilldate).getDays();
// if (daysToBeAccrued < 1) { return; }
int daysInSchedule = Days.daysBetween(interestStartDate, tilldate).getDays();
switch (accrualData.getRepaymentFrequency()) {
case MONTHS:
int numberOfDaysInMonth = calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData);
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysInMonth)) {
totalNumberOfDays = numberOfDaysInMonth;
}
break;
case YEARS:
int numberOfDaysinYear = calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData);
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysinYear)) {
totalNumberOfDays = numberOfDaysinYear;
}
break;
default:
break;
}
/*
* switch (accrualData.getRepaymentFrequency()) { case MONTHS: int
* numberOfDaysInMonth =
* calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData);
* if (!(!isInterestStartDateEqulsInstallmentStartdate &&
* totalNumberOfDays < numberOfDaysInMonth)) { totalNumberOfDays =
* numberOfDaysInMonth; } break; case YEARS: int numberOfDaysinYear =
* calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData); if
* (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays
* < numberOfDaysinYear)) { totalNumberOfDays = numberOfDaysinYear; }
* break; default: break; }
*/
double interestPerDay = accrualData.getInterestIncome().doubleValue() / totalNumberOfDays;
BigDecimal amount = BigDecimal.ZERO;
@@ -232,37 +228,23 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
}
}
private int calculateTotalNumberOfDaysForMonth(final int totalNumberOfDays, final LoanScheduleAccrualData accrualData) {
int numberOfDays = totalNumberOfDays;
DaysInMonthType type = DaysInMonthType.fromInt(accrualData.getNumberOfDaysInMonth());
switch (type) {
case DAYS_30:
numberOfDays = 30 * accrualData.getRepayEvery();
break;
default:
break;
}
return numberOfDays;
}
private int calculateTotalNumberOfDaysForYear(final int totalNumberOfDays, final LoanScheduleAccrualData accrualData) {
int numberOfDays = totalNumberOfDays;
DaysInYearType type = DaysInYearType.fromInt(accrualData.getNumberOfDaysInMonth());
switch (type) {
case DAYS_360:
numberOfDays = 360 * accrualData.getRepayEvery();
break;
case DAYS_364:
numberOfDays = 364 * accrualData.getRepayEvery();
break;
case DAYS_365:
numberOfDays = 365 * accrualData.getRepayEvery();
break;
default:
break;
}
return numberOfDays;
}
/*
* private int calculateTotalNumberOfDaysForMonth(final int
* totalNumberOfDays, final LoanScheduleAccrualData accrualData) { int
* numberOfDays = totalNumberOfDays; DaysInMonthType type =
* DaysInMonthType.fromInt(accrualData.getNumberOfDaysInMonth()); switch
* (type) { case DAYS_30: numberOfDays = 30 * accrualData.getRepayEvery();
* break; default: break; } return numberOfDays; }
*
* private int calculateTotalNumberOfDaysForYear(final int
* totalNumberOfDays, final LoanScheduleAccrualData accrualData) { int
* numberOfDays = totalNumberOfDays; DaysInYearType type =
* DaysInYearType.fromInt(accrualData.getNumberOfDaysInMonth()); switch
* (type) { case DAYS_360: numberOfDays = 360 * accrualData.getRepayEvery();
* break; case DAYS_364: numberOfDays = 364 * accrualData.getRepayEvery();
* break; case DAYS_365: numberOfDays = 365 * accrualData.getRepayEvery();
* break; default: break; } return numberOfDays; }
*/
@Transactional
public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception {
@@ -383,12 +383,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LocalDate earliestUnpaidInstallmentDate = LocalDate.now();
final Money precloseAmount = loan.fetchPrepaymentDetail(this.loanScheduleFactory);
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchPrepaymentDetail(this.loanScheduleFactory);
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
precloseAmount.getAmount(), null, null, null, null, null, paymentOptions, null, null, null);
loanRepaymentScheduleInstallment.getTotalOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment
.getPrincipalOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment.getInterestOutstanding(currency)
.getAmount(), loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, paymentOptions, null, null, null);
}
@Override
@@ -1450,8 +1453,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Long officeId = rs.getLong("officeId");
final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill");
final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom");
final Integer numberOfDaysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");;
final Integer numberOfDaysInYear = JdbcSupport.getInteger(rs, "daysInYear");;
final Integer numberOfDaysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");
;
final Integer numberOfDaysInYear = JdbcSupport.getInteger(rs, "daysInYear");
;
final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum");
final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery");
final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum);
@@ -278,7 +278,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
if (isAccountTransfer) {
disburseLoanToSavings(loan, command, disburseAmount, paymentDetail);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
} else {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId);
disbursementTransaction.updateLoan(loan);
@@ -291,10 +295,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency,
existingTransactionIds, existingReversedTransactionIds, changes, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay,
recalcualteSchedule, restCalendarInstance);
changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, changes,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance);
}
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
@@ -467,7 +470,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
if (isAccountTransfer) {
disburseLoanToSavings(loan, command, disburseAmount, paymentDetail);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
} else {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId);
disbursementTransaction.updateLoan(loan);
@@ -479,10 +487,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency,
existingTransactionIds, existingReversedTransactionIds, changes, firstRepaymentOnDate, isHolidayEnabled, holidays,
workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule,
restCalendarInstance);
changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, changes,
firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance);
}
if (!changes.isEmpty()) {
@@ -44,4 +44,8 @@ public enum AmortizationMethod {
}
return repaymentMethod;
}
public boolean isEqualInstallment() {
return this.value.equals(AmortizationMethod.EQUAL_INSTALLMENTS);
}
}
@@ -24,6 +24,7 @@ public class LoanRepaymentScheduleInstallmentBuilder {
private final Money feeCharges = new MoneyBuilder().build();
private final Money penaltyCharges = new MoneyBuilder().build();
private boolean completed = false;
private boolean recalculatedInterestComponent = false;
public LoanRepaymentScheduleInstallmentBuilder(final MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail;
@@ -34,7 +35,7 @@ public class LoanRepaymentScheduleInstallmentBuilder {
public LoanRepaymentScheduleInstallment build() {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this.loan, this.installmentNumber,
this.fromDate, this.dueDate, this.principal.getAmount(), this.interest.getAmount(), this.feeCharges.getAmount(),
this.penaltyCharges.getAmount());
this.penaltyCharges.getAmount(), this.recalculatedInterestComponent);
if (this.completed) {
installment.payPrincipalComponent(this.latestTransactionDate, this.principal);
installment.payInterestComponent(this.latestTransactionDate, this.interest);