MIFOSX-1490 : added interest schedule for interest first repayment strategies

This commit is contained in:
Pramod Nuthakki
2014-08-28 22:38:20 +05:30
parent 76f064503b
commit 6c6362bf34
19 changed files with 662 additions and 228 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 static org.junit.Assert.assertEquals;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -15,6 +16,7 @@ import java.util.Calendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.joda.time.Days;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@@ -722,9 +724,28 @@ public class AccountingScenarioIntegrationTest {
e.printStackTrace(); e.printStackTrace();
} }
final ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec,
loanID);
// MAKE 1 // MAKE 1
final float FIRST_INTEREST = 200.0f; List fromDateList = (List) loanSchedule.get(1).get("fromDate");
final float INTEREST_4_DAYS = 13.33F; 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, this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(currentDate), INTEREST_4_DAYS, FEE_PORTION,
PENALTY_PORTION, loanID); PENALTY_PORTION, loanID);
@@ -797,8 +818,27 @@ public class AccountingScenarioIntegrationTest {
this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOndate); this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOndate);
final ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec,
loanID);
// MAKE 1 // 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, this.loanTransactionHelper.checkAccrualTransactionForRepayment(getDateAsLocalDate(runOndate), INTEREST_3_DAYS, FEE_PORTION,
PENALTY_PORTION, loanID); PENALTY_PORTION, loanID);
@@ -977,7 +977,7 @@ public class Loan extends AbstractPersistable<Long> {
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue()); scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
addRepaymentScheduleInstallment(installment); addRepaymentScheduleInstallment(installment);
} }
} }
@@ -3774,15 +3774,17 @@ public class Loan extends AbstractPersistable<Long> {
final Set<LoanInstallmentCharge> loanChargePerInstallments = new HashSet<>(); final Set<LoanInstallmentCharge> loanChargePerInstallments = new HashSet<>();
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
BigDecimal amount = BigDecimal.ZERO; if (!installment.isRecalculatedInterestComponent()) {
if (loanCharge.getChargeCalculation().isFlat()) { BigDecimal amount = BigDecimal.ZERO;
amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); if (loanCharge.getChargeCalculation().isFlat()) {
} else { amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments()));
amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment) } else {
.getAmount(); 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; return loanChargePerInstallments;
@@ -4140,8 +4142,8 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
} }
public Money fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) { public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) {
Money amount = Money.zero(getCurrency()); LoanRepaymentScheduleInstallment installment = null;
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
@@ -4177,21 +4179,27 @@ public class Loan extends AbstractPersistable<Long> {
loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(), loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData,
this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate()); this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate());
amount = loanScheduleGenerator.fetchPrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(), loanApplicationTerms, installment = loanScheduleGenerator.calculatePrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(),
mc); loanApplicationTerms, LocalDate.now());
} else { } else {
amount = this.getTotalOutstandingOnLoan(); installment = this.getTotalOutstandingOnLoan();
} }
return amount; return installment;
} }
private Money getTotalOutstandingOnLoan() { private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() {
Money cumulativeOurstanding = Money.zero(loanCurrency()); 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) { 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() { public List<LoanRepaymentScheduleInstallment> fetchRepaymentScheduleInstallments() {
@@ -99,6 +99,9 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
@Column(name = "obligations_met_on_date") @Column(name = "obligations_met_on_date")
private Date obligationsMetOnDate; private Date obligationsMetOnDate;
@Column(name = "recalculated_interest_component", nullable = false)
private boolean recalculatedInterestComponent;
protected LoanRepaymentScheduleInstallment() { protected LoanRepaymentScheduleInstallment() {
this.installmentNumber = null; this.installmentNumber = null;
this.fromDate = 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, public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges) { final BigDecimal penaltyCharges, boolean recalculatedInterestComponent) {
this.loan = loan; this.loan = loan;
this.installmentNumber = installmentNumber; this.installmentNumber = installmentNumber;
this.fromDate = fromDate.toDateTimeAtStartOfDay().toDate(); this.fromDate = fromDate.toDateTimeAtStartOfDay().toDate();
@@ -118,6 +121,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.feeChargesCharged = defaultToNullIfZero(feeCharges); this.feeChargesCharged = defaultToNullIfZero(feeCharges);
this.penaltyCharges = defaultToNullIfZero(penaltyCharges); this.penaltyCharges = defaultToNullIfZero(penaltyCharges);
this.obligationsMet = false; this.obligationsMet = false;
this.recalculatedInterestComponent = recalculatedInterestComponent;
} }
private BigDecimal defaultToNullIfZero(final BigDecimal value) { private BigDecimal defaultToNullIfZero(final BigDecimal value) {
@@ -540,4 +544,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
public Money getTotalPaidLate(final MonetaryCurrency currency) { public Money getTotalPaidLate(final MonetaryCurrency currency) {
return Money.of(currency, this.totalPaidLate); 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) { for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, 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, final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges,
currency); currency, !period.isRecalculatedInterestComponent());
final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(),
loanCharges, currency); loanCharges, currency, !period.isRecalculatedInterestComponent());
final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), 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(), final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(),
loanCharges, currency); loanCharges, currency, !period.isRecalculatedInterestComponent());
final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate,
period.getDueDate(), loanCharges, currency); period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent());
period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod,
feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod,
@@ -53,13 +53,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges, private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, int numberOfRepayments, final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, int numberOfRepayments,
final Money totalInterest) { final Money totalInterest, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(monetaryCurrency); Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) { if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -75,11 +75,11 @@ public class LoanRepaymentScheduleProcessingWrapper {
} else { } else {
cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments)));
} }
}else if (loanCharge.isOverdueInstallmentCharge() } else if (loanCharge.isOverdueInstallmentCharge()
&& loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) { && loanCharge.getChargeCalculation().isPercentageBased()) {
cumulative = cumulative.plus(loanCharge.chargeAmount()); cumulative = cumulative.plus(loanCharge.chargeAmount());
}else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) { && loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -101,13 +101,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
} }
private Money cumulativeFeeChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, 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); Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) { if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) { if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency));
@@ -122,13 +122,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
} }
private Money cumulativeFeeChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, 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); Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) { if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) { if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency));
@@ -144,13 +144,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period,
int numberOfRepayments, Money totalInterest) { int numberOfRepayments, Money totalInterest, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(currency); Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) { if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -166,7 +166,7 @@ public class LoanRepaymentScheduleProcessingWrapper {
} else { } else {
cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments)));
} }
}else if (loanCharge.isOverdueInstallmentCharge() } else if (loanCharge.isOverdueInstallmentCharge()
&& loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)
&& loanCharge.getChargeCalculation().isPercentageBased()) { && loanCharge.getChargeCalculation().isPercentageBased()) {
cumulative = cumulative.plus(loanCharge.chargeAmount()); cumulative = cumulative.plus(loanCharge.chargeAmount());
@@ -192,13 +192,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
} }
private Money cumulativePenaltyChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, 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); Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) { if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) { if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency));
@@ -213,13 +213,13 @@ public class LoanRepaymentScheduleProcessingWrapper {
} }
private Money cumulativePenaltyChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, 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); Money cumulative = Money.zero(currency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) { if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd);
if (loanChargePerInstallment != null) { if (loanChargePerInstallment != null) {
cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency));
@@ -6,7 +6,6 @@
package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor; package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; 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.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
/** /**
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} * Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor}
@@ -438,29 +438,51 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
@Override @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 MonetaryCurrency currency, final List<LoanRepaymentScheduleInstallment> installments,
final LoanRepaymentScheduleInstallment installment, final Map<LocalDate, LocalDate> recalculationDates) { final LoanRepaymentScheduleInstallment installment, final Map<LocalDate, LocalDate> recalculationDates,
Map<LocalDate, Money> unprocessedMap = new HashMap<>(); final LoanTransaction preCloseTransaction) {
List<RecalculationDetail> earlypaymentDetail = new ArrayList<>();
Money currentInstallmentOutstanding = installment.getTotalOutstanding(currency); Money currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null; Money amountToProcess = null;
LocalDate transactionDate = loanTransaction.getTransactionDate();
if (loanTransaction != preCloseTransaction) {
transactionDate = recalculationDates.get(transactionDate);
}
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
newLoanTransaction.resetDerivedComponents(); newLoanTransaction.resetDerivedComponents();
Money unProcessed = processTransaction(newLoanTransaction, currency, installments, amountToProcess); Money unProcessed = processTransaction(newLoanTransaction, currency, installments, amountToProcess);
if (loanTransaction.getTransactionDate().isAfter(installment.getFromDate()) if (transactionDate.isAfter(installment.getFromDate()) && transactionDate.isBefore(installment.getDueDate())) {
&& recalculationDates.get(loanTransaction.getTransactionDate()).isBefore(installment.getDueDate())) {
Money earlyPayment = currentInstallmentOutstanding.minus(installment.getTotalOutstanding(currency)); Money earlyPayment = currentInstallmentOutstanding.minus(installment.getTotalOutstanding(currency));
if (earlyPayment.isGreaterThanZero()) { if (earlyPayment.isGreaterThanZero()) {
unprocessedMap.put(loanTransaction.getTransactionDate(), earlyPayment); earlypaymentDetail.add(new RecalculationDetail(false, transactionDate, null, earlyPayment, false));
} }
currentInstallmentOutstanding = installment.getTotalOutstanding(currency); currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
} }
if (unProcessed.isGreaterThanZero()) { 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.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; 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.ChangedTransactionDetail;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
public interface LoanRepaymentScheduleTransactionProcessor { public interface LoanRepaymentScheduleTransactionProcessor {
@@ -28,8 +28,16 @@ public interface LoanRepaymentScheduleTransactionProcessor {
void handleWriteOff(LoanTransaction loanTransaction, MonetaryCurrency loanCurrency, void handleWriteOff(LoanTransaction loanTransaction, MonetaryCurrency loanCurrency,
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments); 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, 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) { protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment // 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; LocalDate periodStartDateApplicableForInterest = periodStartDate;
int periodNumber = 1; int periodNumber = 1;
int instalmentNumber = 1;
Money totalCumulativePrincipal = principalDisbursed.zero(); Money totalCumulativePrincipal = principalDisbursed.zero();
Money totalCumulativeInterest = principalDisbursed.zero(); Money totalCumulativeInterest = principalDisbursed.zero();
Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero(); Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero();
@@ -109,12 +110,43 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
Money reducePrincipal = totalCumulativePrincipal.zero(); Money reducePrincipal = totalCumulativePrincipal.zero();
Money fixedEmiAmount = totalCumulativePrincipal.zero(); Money fixedEmiAmount = totalCumulativePrincipal.zero();
int daysCalcForInstallmentNumber = 0;
LocalDate scheduleStartDateAsPerFrequency = periodStartDate;
while (!outstandingBalance.isZero()) { while (!outstandingBalance.isZero()) {
isFirstRepayment = false; isFirstRepayment = false;
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms, // to insert a new schedule between actual schedule to collect
isFirstRepayment); // interest(till the transaction date) first on a payment
LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, boolean recalculatedInterestComponent = false;
isHolidayEnabled, holidays, workingDays); 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(); final int daysInPeriod = Days.daysBetween(periodStartDate, scheduledDueDate).getDays();
if (loanApplicationTerms.isMultiDisburseLoan()) { if (loanApplicationTerms.isMultiDisburseLoan()) {
loanApplicationTerms.setFixedEmiAmountForPeriod(scheduledDueDate); loanApplicationTerms.setFixedEmiAmountForPeriod(scheduledDueDate);
@@ -141,10 +173,27 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDateApplicableForInterest, scheduledDueDate).getDays(); daysInPeriodApplicableForInterest = Days.daysBetween(periodStartDateApplicableForInterest, scheduledDueDate).getDays();
} }
final double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator // this block identifies full payment period as per the
.calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, scheduledDueDate, // schedule(excluding interest payments) for interest calculation
loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), daysInPeriodApplicableInFullInstallment = daysInPeriodApplicableForInterest;
loanApplicationTerms.getRepaymentEvery()); 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; Money balanceForcalculation = outstandingBalance;
// reduce principal is the early payment, will processed as per the // reduce principal is the early payment, will processed as per the
// reschedule strategy // reschedule strategy
@@ -177,10 +226,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
// 5 determine principal,interest of repayment period // 5 determine principal,interest of repayment period
final PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(
this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal, this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal,
totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace,
daysInPeriodApplicableForInterest, balanceForcalculation, loanApplicationTerms, periodNumber, mc); daysInPeriodApplicableInFullInstallment, balanceForcalculation, loanApplicationTerms, periodNumber, mc);
if (loanApplicationTerms.getFixedEmiAmount() != null if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1) { && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1) {
@@ -190,17 +239,34 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
// update cumulative fields for principal & interest // update cumulative fields for principal & interest
Money interestForThisinstallment = principalInterestForThisPeriod.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); totalCumulativeInterest = totalCumulativeInterest.plus(interestForThisinstallment);
totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace(); totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace();
// 6. update outstandingLoanBlance using correct 'principalDue'
outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal());
Money actualOutstandingbalance = outstandingBalance; Money actualOutstandingbalance = outstandingBalance;
Money feeChargesForInstallment = principalDisbursed.zero(); Money feeChargesForInstallment = principalDisbursed.zero();
Money penaltyChargesForInstallment = principalDisbursed.zero(); Money penaltyChargesForInstallment = principalDisbursed.zero();
Money principalForThisPeriod = principalInterestForThisPeriod.principal();
if (principalForThisPeriod.isGreaterThan(reducePrincipal)) { if (principalForThisPeriod.isGreaterThan(reducePrincipal)) {
principalForThisPeriod = principalForThisPeriod.minus(reducePrincipal); principalForThisPeriod = principalForThisPeriod.minus(reducePrincipal);
reducePrincipal = reducePrincipal.zero(); reducePrincipal = reducePrincipal.zero();
@@ -213,51 +279,87 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment); fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment);
} }
Money extraPrincipal = reducePrincipal.zero();
// this block is to identify interest based on late/early payment // this block is to identify interest based on late/early payment
if (diffAmt != null && !diffAmt.isEmpty()) { if (diffAmt != null && !diffAmt.isEmpty()) {
BigDecimal interestDueToLatePayment = BigDecimal.ZERO; BigDecimal interestDueToLatePayment = BigDecimal.ZERO;
BigDecimal interestReducedDueToEarlyPayment = BigDecimal.ZERO; BigDecimal interestReducedDueToEarlyPayment = BigDecimal.ZERO;
boolean isperiodPrincipalReduced = false;
for (RecalculationDetail detail : diffAmt) { for (RecalculationDetail detail : diffAmt) {
// will increase the principal portion and reduces interest if (!detail.isInterestompound()) {
// as per the number of days. and also identifies reduce // will increase the principal portion and reduces
// principal for reschedule strategy // interest
if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate) // as per the number of days. and also identifies reduce
&& !detail.getStartDate().isAfter(scheduledDueDate)) { // principal for reschedule strategy
reducePrincipal = reducePrincipal.plus(detail.getAmount()); if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate)
int diffDays = Days.daysBetween(detail.getStartDate(), scheduledDueDate).getDays(); && !detail.getStartDate().isAfter(scheduledDueDate)) {
if (diffDays > 0) { reducePrincipal = reducePrincipal.plus(detail.getAmount());
reducePrincipal = reducePrincipal.minus(principalForThisPeriod); 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()) { // calculates the interest for late payment and increase
reducePrincipal = reducePrincipal.zero(); // the interest for the installment
} else { else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate)
Money actualOutstanding = outstandingBalance.minus(reducePrincipal); && periodStartDate.isBefore(LocalDate.now())) {
principalForThisPeriod = reducePrincipal.plus(principalForThisPeriod); LocalDate fromDate = periodStartDate;
if (actualOutstanding.isLessThanZero()) { LocalDate toDate = scheduledDueDate;
principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding); 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) if (reducePrincipal.isLessThanZero()) {
&& periodStartDate.isBefore(LocalDate.now())) { reducePrincipal = reducePrincipal.zero();
LocalDate fromDate = periodStartDate; } else {
LocalDate toDate = scheduledDueDate; Money actualOutstanding = outstandingBalance.minus(reducePrincipal);
if (!detail.getStartDate().isBefore(periodStartDate)) { principalForThisPeriod = reducePrincipal.plus(principalForThisPeriod);
fromDate = detail.getStartDate(); if (actualOutstanding.isLessThanZero()) {
} principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding);
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));
} }
} }
@@ -281,9 +383,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
.plus(penaltyChargesForInstallment); .plus(penaltyChargesForInstallment);
// 9. create repayment period from parts // 9. create repayment period from parts
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, periodStartDate, final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(instalmentNumber, periodStartDate,
scheduledDueDate, principalForThisPeriod, actualOutstandingbalance, interestForThisinstallment, scheduledDueDate, principalForThisPeriod, actualOutstandingbalance, interestForThisinstallment,
feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue); feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue, recalculatedInterestComponent);
periods.add(installment); periods.add(installment);
// handle cumulative fields // handle cumulative fields
@@ -296,7 +398,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
periodStartDate = scheduledDueDate; periodStartDate = scheduledDueDate;
periodStartDateApplicableForInterest = periodStartDate; periodStartDateApplicableForInterest = periodStartDate;
periodNumber++; if (!recalculatedInterestComponent) {
periodNumber++;
scheduleStartDateAsPerFrequency = periodStartDate;
}
instalmentNumber++;
} }
if (principalDisbursed.isNotEqualTo(expectedPrincipalDisburse)) { if (principalDisbursed.isNotEqualTo(expectedPrincipalDisburse)) {
@@ -311,10 +417,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Money.of(currency, loanScheduleModelPeriod.interestDue()), null); Money.of(currency, loanScheduleModelPeriod.interestDue()), null);
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed, loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed,
totalCumulativeInterest, numberOfRepayments); totalCumulativeInterest, numberOfRepayments, !loanScheduleModelPeriod.isRecalculatedInterestComponent());
Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed, loanScheduleModelPeriod.periodDueDate(), loanCharges, currency, principalInterest, principalDisbursed,
totalCumulativeInterest, numberOfRepayments); totalCumulativeInterest, numberOfRepayments, !loanScheduleModelPeriod.isRecalculatedInterestComponent());
totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount());
totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount());
loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), 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 // this block is to add extra re-payment schedules with interest portion
// if the last payment is missed // if the last payment is missed
if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) { if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) {
boolean recalculatedInterestComponent = true;
Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>(); Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>();
for (RecalculationDetail detail : diffAmt) { 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())) { if (processDetails.containsKey(detail.getToDate())) {
RecalculationDetail recalculationDetail = processDetails.get(detail.getToDate()); RecalculationDetail recalculationDetail = processDetails.get(detail.getToDate());
RecalculationDetail updatedDetail = new RecalculationDetail(recalculationDetail.isLatePayment(), RecalculationDetail updatedDetail = new RecalculationDetail(recalculationDetail.isLatePayment(),
recalculationDetail.getStartDate(), recalculationDetail.getToDate(), recalculationDetail.getAmount().plus( recalculationDetail.getStartDate(), recalculationDetail.getToDate(), recalculationDetail.getAmount().plus(
detail.getAmount())); detail.getAmount()), detail.isInterestompound());
processDetails.put(updatedDetail.getToDate(), updatedDetail); processDetails.put(updatedDetail.getToDate(), updatedDetail);
} else { } else {
processDetails.put(detail.getToDate(), detail); processDetails.put(detail.getToDate(), detail);
@@ -350,11 +457,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
totalInterestCharged = totalInterestCharged.add(interestDueToLatePayment.getAmount()); totalInterestCharged = totalInterestCharged.add(interestDueToLatePayment.getAmount());
totalRepaymentExpected = totalRepaymentExpected.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, toDate, interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment,
interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment); interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment,
recalculatedInterestComponent);
periods.add(installment); periods.add(installment);
periodNumber++; instalmentNumber++;
} }
} }
} }
@@ -365,6 +473,23 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
totalOutstanding); 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, public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator,
double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest,
Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, int daysInPeriodApplicableForInterest, 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, private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, 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); Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) { if (loanCharge.isFeeCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -482,13 +608,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
final Set<LoanCharge> loanCharges, final MonetaryCurrency monetaryCurrency, final Set<LoanCharge> loanCharges, final MonetaryCurrency monetaryCurrency,
final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments) { final Money totalInterestChargedForFullLoanTerm, int numberOfRepayments, boolean isInstallmentChargeApplicable) {
Money cumulative = Money.zero(monetaryCurrency); Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) { if (loanCharge.isPenaltyCharge()) {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
@@ -544,9 +670,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
LoanScheduleModel loanScheduleModel = null; LoanScheduleModel loanScheduleModel = null;
List<LoanRepaymentScheduleInstallment> installments = previousSchedule; List<LoanRepaymentScheduleInstallment> installments = previousSchedule;
List<LoanRepaymentScheduleInstallment> removeInstallments = new ArrayList<>(); List<LoanRepaymentScheduleInstallment> removeInstallments = new ArrayList<>();
MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency();
LoanTransaction preCloseTransaction = getPreclosureTransaction(transactions, installments, currency, loanApplicationTerms);
for (LoanRepaymentScheduleInstallment installment : installments) { for (LoanRepaymentScheduleInstallment installment : installments) {
if (!loanApplicationTerms.isPrincipalGraceApplicableForThisPeriod(installment.getInstallmentNumber()) if (installment.isRecalculatedInterestComponent() && recalculateFrom != null
&& installment.getPrincipal(loanApplicationTerms.getCurrency()).isZero()) { && !recalculateFrom.isAfter(installment.getDueDate())) {
removeInstallments.add(installment); removeInstallments.add(installment);
} }
} }
@@ -556,22 +684,129 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
workingDays); workingDays);
installments = retrieveRepaymentSchedule(loanScheduleModel); installments = retrieveRepaymentSchedule(loanScheduleModel);
} }
if (recalculateFrom == null) { if (recalculateFrom == null) {
recalculateFrom = loanApplicationTerms.getExpectedDisbursementDate(); 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())) { while (!recalculateFrom.isAfter(LocalDate.now())) {
int installmentNumber = findLastProcessedInstallmentNumber(installments, recalculateFrom); int installmentNumber = findLastProcessedInstallmentNumber(installments, recalculateFrom);
List<LoanTransaction> processTransactions = processTransactions(transactions, 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, loanScheduleModel = recalculateInstallment(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled,
holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule, loanScheduleModel, installments, holidays, workingDays, loanRepaymentScheduleTransactionProcessor, previousSchedule, loanScheduleModel, installments,
processTransactions, installmentNumber); processTransactions, installmentNumber, recalculationDetails, preCloseTransaction);
if (loanScheduleModel != null) {
installments = retrieveRepaymentSchedule(loanScheduleModel);
}
recalculateFrom = getNextRecalculateFromDate(transactions, recalculateFrom); recalculateFrom = getNextRecalculateFromDate(transactions, recalculateFrom);
} }
return loanScheduleModel; 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) { private LocalDate getNextRecalculateFromDate(List<LoanTransaction> processTransactions, LocalDate preCalculationDate) {
LocalDate recalculateFrom = null; LocalDate recalculateFrom = null;
for (LoanTransaction loanTransaction : processTransactions) { for (LoanTransaction loanTransaction : processTransactions) {
@@ -590,25 +825,28 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
* Method calls regenerate schedule for a particular set of transactions * Method calls regenerate schedule for a particular set of transactions
* till all the installments are processed to identify early or late * till all the installments are processed to identify early or late
* payments and will be used in regeneration of schedule * payments and will be used in regeneration of schedule
*
* @param preCloseTransaction
* TODO
*/ */
private LoanScheduleModel recalculateInstallment(final MathContext mc, final ApplicationCurrency applicationCurrency, private LoanScheduleModel recalculateInstallment(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<Holiday> holidays, final WorkingDays workingDays,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> previousSchedule, LoanScheduleModel loanScheduleModel, 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; int processInstallmentsFrom = 0;
Integer numberOfInstallments = previousSchedule.size() - 1; Integer lastInstallmentNumber = previousSchedule.size();
final List<RecalculationDetail> recalculationDetails = new ArrayList<>(); while (processInstallmentsFrom < lastInstallmentNumber) {
while (processInstallmentsFrom < numberOfInstallments) {
RecalculatedSchedule recalculatedSchedule = recalculateInterest(mc, applicationCurrency, loanApplicationTerms, loanCharges, RecalculatedSchedule recalculatedSchedule = recalculateInterest(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, processTransactions, loanRepaymentScheduleTransactionProcessor, isHolidayEnabled, holidays, workingDays, processTransactions, loanRepaymentScheduleTransactionProcessor,
processInstallmentsFrom, installments, recalculationDetails, skipRecalculation); processInstallmentsFrom, installments, recalculationDetails, skipRecalculation, preCloseTransaction);
processInstallmentsFrom = recalculatedSchedule.getInstallmentNumber(); processInstallmentsFrom = recalculatedSchedule.getInstallmentNumber();
if (recalculatedSchedule.getLoanScheduleModel() != null) { if (recalculatedSchedule.getLoanScheduleModel() != null) {
loanScheduleModel = recalculatedSchedule.getLoanScheduleModel(); loanScheduleModel = recalculatedSchedule.getLoanScheduleModel();
installments = retrieveRepaymentSchedule(loanScheduleModel); installments = retrieveRepaymentSchedule(loanScheduleModel);
numberOfInstallments = installments.size() - 1; lastInstallmentNumber = installments.size() - 1;
} }
} }
return loanScheduleModel; return loanScheduleModel;
@@ -616,13 +854,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
/** /**
* Method identifies late or early payments for schedule recalculation. * Method identifies late or early payments for schedule recalculation.
*
* @param preCloseTransaction
* TODO
*/ */
private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency, private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions, final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final int installmentNumber, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final int installmentNumber,
final List<LoanRepaymentScheduleInstallment> installments, final List<RecalculationDetail> recalculationDetails, final List<LoanRepaymentScheduleInstallment> installments, final List<RecalculationDetail> recalculationDetails,
final int skipRecalculation) { final int skipRecalculation, LoanTransaction preCloseTransaction) {
boolean processRecalculate = false; boolean processRecalculate = false;
int processedInstallmentNumber = installmentNumber; int processedInstallmentNumber = installmentNumber;
@@ -642,9 +883,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
continue; continue;
} }
processedInstallmentNumber = installment.getInstallmentNumber(); processedInstallmentNumber = installment.getInstallmentNumber();
if (installment.getDueDate().isAfter(LocalDate.now())) { if (installment.getInstallmentNumber() == installments.size()) {
continue; processRecalculate = true;
} }
List<LoanTransaction> transactionsForInstallment = new ArrayList<>(); List<LoanTransaction> transactionsForInstallment = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>(); Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
LocalDate transactionsDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms, LocalDate transactionsDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms,
@@ -659,19 +901,18 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
workingDays)); 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 // this block is to create early payment entries for schedule
// generation // generation
for (Map.Entry<LocalDate, Money> entry : earlyPaymentMap.entrySet()) { for (RecalculationDetail recalculationDetail : earlypaymentDetail) {
LocalDate startDate = entry.getKey(); if (!recalculationDetail.getStartDate().isAfter(installment.getDueDate())
if (unpaidPricipal.isGreaterThan(entry.getValue())) { && recalculationDetail.getStartDate().isAfter(installment.getFromDate())) {
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())) {
diffAmt.add(recalculationDetail); diffAmt.add(recalculationDetail);
} }
} }
@@ -692,7 +933,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
startDate = startDate.minusDays(1); startDate = startDate.minusDays(1);
} }
applyRest(transactions, startDate, recalculateFrom, installment, loanRepaymentScheduleTransactionProcessor, currency, applyRest(transactions, startDate, recalculateFrom, installment, loanRepaymentScheduleTransactionProcessor, currency,
loanApplicationTerms, isHolidayEnabled, holidays, workingDays, installments); loanApplicationTerms, isHolidayEnabled, holidays, workingDays, installments, preCloseTransaction);
totalOutstanding = installment.getTotalOutstanding(currency); totalOutstanding = installment.getTotalOutstanding(currency);
if (totalOutstanding.isGreaterThanZero()) { if (totalOutstanding.isGreaterThanZero()) {
Money latepaymentoutstanding = installment.getPrincipalOutstanding(currency); Money latepaymentoutstanding = installment.getPrincipalOutstanding(currency);
@@ -714,7 +955,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
break; break;
} }
RecalculationDetail recalculationDetail = new RecalculationDetail(true, recalculateFrom, recalcualteTill, RecalculationDetail recalculationDetail = new RecalculationDetail(true, recalculateFrom, recalcualteTill,
latepaymentoutstanding); latepaymentoutstanding, false);
diffAmt.add(recalculationDetail); diffAmt.add(recalculationDetail);
} }
startDate = recalculateFrom.plusDays(1); startDate = recalculateFrom.plusDays(1);
@@ -722,8 +963,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
} }
if ((!diffAmt.isEmpty() && skipRecalculation <= installment.getInstallmentNumber()) if (!diffAmt.isEmpty() && skipRecalculation <= installment.getInstallmentNumber()) {
|| installment.getInstallmentNumber() == installments.size() - 1) {
processRecalculate = true; processRecalculate = true;
} }
break; break;
@@ -746,7 +986,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue()); scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
installments.add(installment); installments.add(installment);
} }
} }
@@ -756,12 +996,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
/** /**
* Method to identify which transaction did the payment for current * Method to identify which transaction did the payment for current
* installment * installment
*
* @param preCloseTransaction
* TODO
*/ */
private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to, private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to,
LoanRepaymentScheduleInstallment installment, LoanRepaymentScheduleInstallment installment,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency,
final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, final List<Holiday> holidays, 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<>(); List<LoanTransaction> transactions = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>(); Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
for (LoanTransaction transaction : loanTransactions) { for (LoanTransaction transaction : loanTransactions) {
@@ -776,7 +1019,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(transactions, currency, installments, installment, loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(transactions, currency, installments, installment,
recalculationDates); recalculationDates, preCloseTransaction);
} }
@SuppressWarnings("unused") @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. * Method returns the amount payable to close the loan account as of today.
*/ */
@Override @Override
public Money fetchPrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments,
final LoanApplicationTerms applicationTerms, final MathContext mc) { MonetaryCurrency currency, final LoanApplicationTerms applicationTerms, final LocalDate onDate) {
Money prepaymentAmount = Money.zero(currency); Money outstandingPrincipal = Money.zero(currency);
Money amount = Money.zero(currency); Money feeCharges = Money.zero(currency);
LocalDate calculateInterestFrom = LocalDate.now(); 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) { for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyPaidOff()) { if (currentInstallment.isNotFullyPaidOff()) {
if (!currentInstallment.getDueDate().isAfter(LocalDate.now())) { if (!currentInstallment.getDueDate().isAfter(onDate)) {
amount = amount.plus(currentInstallment.getTotalOutstanding(currency)); 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 { } 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)) { if (currentInstallment.getFromDate().isBefore(calculateInterestFrom)) {
calculateInterestFrom = currentInstallment.getFromDate(); 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, totalInterest = totalInterest.plus(interest);
calculateInterestFrom, LocalDate.now()); totalPrincipal = totalPrincipal.plus(outstandingPrincipal);
prepaymentAmount = prepaymentAmount.plus(interest).plus(amount);
return prepaymentAmount; 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) { private Integer findLastProcessedInstallmentNumber(final List<LoanRepaymentScheduleInstallment> installments, LocalDate date) {
@@ -832,6 +1100,27 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return installmentNumber; 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) { private List<LoanTransaction> processTransactions(final List<LoanTransaction> transactions, final LocalDate tillDate) {
List<LoanTransaction> toProcess = new ArrayList<>(); List<LoanTransaction> toProcess = new ArrayList<>();
for (LoanTransaction loanTransaction : transactions) { for (LoanTransaction loanTransaction : transactions) {
@@ -842,4 +1131,24 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return toProcess; 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); 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, private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod, 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.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays; import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -31,6 +30,6 @@ public interface LoanScheduleGenerator {
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
List<LoanRepaymentScheduleInstallment> previousSchedule, LocalDate recalculateFrom); List<LoanRepaymentScheduleInstallment> previousSchedule, LocalDate recalculateFrom);
Money fetchPrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
LoanApplicationTerms applicationTerms, MathContext mc); 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) { public void addLoanCharges(@SuppressWarnings("unused") BigDecimal feeCharge, @SuppressWarnings("unused") BigDecimal penaltyCharge) {
return; return;
} }
@Override
public boolean isRecalculatedInterestComponent() {
return false;
}
} }
@@ -31,4 +31,6 @@ public interface LoanScheduleModelPeriod {
BigDecimal penaltyChargesDue(); BigDecimal penaltyChargesDue();
void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge); void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge);
boolean isRecalculatedInterestComponent();
} }
@@ -26,18 +26,19 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
private Money feeChargesDue; private Money feeChargesDue;
private Money penaltyChargesDue; private Money penaltyChargesDue;
private Money totalDue; private Money totalDue;
private final boolean recalculatedInterestComponent;
public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate, public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate,
final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, 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, 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, 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 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.periodNumber = periodNumber;
this.fromDate = fromDate; this.fromDate = fromDate;
this.dueDate = dueDate; this.dueDate = dueDate;
@@ -47,6 +48,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
this.feeChargesDue = feeChargesDue; this.feeChargesDue = feeChargesDue;
this.penaltyChargesDue = penaltyChargesDue; this.penaltyChargesDue = penaltyChargesDue;
this.totalDue = totalDue; this.totalDue = totalDue;
this.recalculatedInterestComponent = recalculatedInterestComponent;
} }
@Override @Override
@@ -122,4 +124,9 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge); this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge);
this.totalDue = this.totalDue.plus(feeCharge).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 { public class RecalculationDetail {
private boolean isLatePayment; private boolean isLatePayment;
private boolean isInterestompound;
private LocalDate startDate; private LocalDate startDate;
private LocalDate toDate; private LocalDate toDate;
private Money amount; 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.isLatePayment = isLatePayment;
this.startDate = startDate; this.startDate = startDate;
this.toDate = toDate; this.toDate = toDate;
this.amount = amount; this.amount = amount;
this.isInterestompound = isInterestompound;
} }
public boolean isLatePayment() { public boolean isLatePayment() {
@@ -42,8 +45,11 @@ public class RecalculationDetail {
return !this.startDate.isAfter(endDate) && !startDate.isAfter(this.toDate); return !this.startDate.isAfter(endDate) && !startDate.isAfter(this.toDate);
} }
public void updateAmount(Money amount) { public void updateAmount(Money amount) {
this.amount = 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.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException; import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
import org.mifosplatform.infrastructure.jobs.service.JobName; 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.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; 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())) { if (accruredTill == null || lastLoanId == null || !lastLoanId.equals(accrualData.getLoanId())) {
accruredTill = accrualData.getAccruedTill(); accruredTill = accrualData.getAccruedTill();
} }
addAccrualTillSpecificDate(tilldate, accrualData, accruredTill); if (accruredTill == null || accruredTill.isBefore(tilldate)) {
addAccrualTillSpecificDate(tilldate, accrualData, accruredTill);
}
} else { } else {
addAccrualAccounting(accrualData); addAccrualAccounting(accrualData);
accruredTill = accrualData.getDueDateAsLocaldate(); accruredTill = accrualData.getDueDateAsLocaldate();
@@ -131,10 +131,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData, private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData,
final LocalDate accruredTill) throws Exception { final LocalDate accruredTill) throws Exception {
LocalDate interestStartDate = accrualData.getFromDateAsLocaldate(); LocalDate interestStartDate = accrualData.getFromDateAsLocaldate();
boolean isInterestStartDateEqulsInstallmentStartdate = true; // boolean isInterestStartDateEqulsInstallmentStartdate = true;
if (accrualData.getInterestCalculatedFrom() != null if (accrualData.getInterestCalculatedFrom() != null
&& accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) { && accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) {
isInterestStartDateEqulsInstallmentStartdate = false; // isInterestStartDateEqulsInstallmentStartdate = false;
if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) { if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) {
interestStartDate = accrualData.getInterestCalculatedFrom(); interestStartDate = accrualData.getInterestCalculatedFrom();
} else { } else {
@@ -157,22 +157,18 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
int daysToBeAccrued = Days.daysBetween(startDate, tilldate).getDays(); int daysToBeAccrued = Days.daysBetween(startDate, tilldate).getDays();
// if (daysToBeAccrued < 1) { return; } // if (daysToBeAccrued < 1) { return; }
int daysInSchedule = Days.daysBetween(interestStartDate, tilldate).getDays(); int daysInSchedule = Days.daysBetween(interestStartDate, tilldate).getDays();
switch (accrualData.getRepaymentFrequency()) { /*
case MONTHS: * switch (accrualData.getRepaymentFrequency()) { case MONTHS: int
int numberOfDaysInMonth = calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData); * numberOfDaysInMonth =
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysInMonth)) { * calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData);
totalNumberOfDays = numberOfDaysInMonth; * if (!(!isInterestStartDateEqulsInstallmentStartdate &&
} * totalNumberOfDays < numberOfDaysInMonth)) { totalNumberOfDays =
break; * numberOfDaysInMonth; } break; case YEARS: int numberOfDaysinYear =
case YEARS: * calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData); if
int numberOfDaysinYear = calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData); * (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysinYear)) { * < numberOfDaysinYear)) { totalNumberOfDays = numberOfDaysinYear; }
totalNumberOfDays = numberOfDaysinYear; * break; default: break; }
} */
break;
default:
break;
}
double interestPerDay = accrualData.getInterestIncome().doubleValue() / totalNumberOfDays; double interestPerDay = accrualData.getInterestIncome().doubleValue() / totalNumberOfDays;
BigDecimal amount = BigDecimal.ZERO; 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; * private int calculateTotalNumberOfDaysForMonth(final int
DaysInMonthType type = DaysInMonthType.fromInt(accrualData.getNumberOfDaysInMonth()); * totalNumberOfDays, final LoanScheduleAccrualData accrualData) { int
switch (type) { * numberOfDays = totalNumberOfDays; DaysInMonthType type =
case DAYS_30: * DaysInMonthType.fromInt(accrualData.getNumberOfDaysInMonth()); switch
numberOfDays = 30 * accrualData.getRepayEvery(); * (type) { case DAYS_30: numberOfDays = 30 * accrualData.getRepayEvery();
break; * break; default: break; } return numberOfDays; }
default: *
break; * private int calculateTotalNumberOfDaysForYear(final int
} * totalNumberOfDays, final LoanScheduleAccrualData accrualData) { int
return numberOfDays; * numberOfDays = totalNumberOfDays; DaysInYearType type =
} * DaysInYearType.fromInt(accrualData.getNumberOfDaysInMonth()); switch
* (type) { case DAYS_360: numberOfDays = 360 * accrualData.getRepayEvery();
private int calculateTotalNumberOfDaysForYear(final int totalNumberOfDays, final LoanScheduleAccrualData accrualData) { * break; case DAYS_364: numberOfDays = 364 * accrualData.getRepayEvery();
int numberOfDays = totalNumberOfDays; * break; case DAYS_365: numberOfDays = 365 * accrualData.getRepayEvery();
DaysInYearType type = DaysInYearType.fromInt(accrualData.getNumberOfDaysInMonth()); * break; default: break; } return numberOfDays; }
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 @Transactional
public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception { public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception {
@@ -383,12 +383,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LocalDate earliestUnpaidInstallmentDate = LocalDate.now(); 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 LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName); .retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, 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 @Override
@@ -1450,8 +1453,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Long officeId = rs.getLong("officeId"); final Long officeId = rs.getLong("officeId");
final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill");
final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom"); final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom");
final Integer numberOfDaysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");; final Integer numberOfDaysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");
final Integer numberOfDaysInYear = JdbcSupport.getInteger(rs, "daysInYear");; ;
final Integer numberOfDaysInYear = JdbcSupport.getInteger(rs, "daysInYear");
;
final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum"); final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum");
final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery"); final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery");
final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum); final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum);
@@ -44,4 +44,8 @@ public enum AmortizationMethod {
} }
return repaymentMethod; 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 feeCharges = new MoneyBuilder().build();
private final Money penaltyCharges = new MoneyBuilder().build(); private final Money penaltyCharges = new MoneyBuilder().build();
private boolean completed = false; private boolean completed = false;
private boolean recalculatedInterestComponent = false;
public LoanRepaymentScheduleInstallmentBuilder(final MonetaryCurrency currencyDetail) { public LoanRepaymentScheduleInstallmentBuilder(final MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail; this.currencyDetail = currencyDetail;
@@ -34,7 +35,7 @@ public class LoanRepaymentScheduleInstallmentBuilder {
public LoanRepaymentScheduleInstallment build() { public LoanRepaymentScheduleInstallment build() {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this.loan, this.installmentNumber, final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this.loan, this.installmentNumber,
this.fromDate, this.dueDate, this.principal.getAmount(), this.interest.getAmount(), this.feeCharges.getAmount(), this.fromDate, this.dueDate, this.principal.getAmount(), this.interest.getAmount(), this.feeCharges.getAmount(),
this.penaltyCharges.getAmount()); this.penaltyCharges.getAmount(), this.recalculatedInterestComponent);
if (this.completed) { if (this.completed) {
installment.payPrincipalComponent(this.latestTransactionDate, this.principal); installment.payPrincipalComponent(this.latestTransactionDate, this.principal);
installment.payInterestComponent(this.latestTransactionDate, this.interest); installment.payInterestComponent(this.latestTransactionDate, this.interest);