support flexible repayment strategies for loans.

This commit is contained in:
Keith Woodlock
2012-07-28 01:47:54 +01:00
parent ef12c864ab
commit 89230969ea
20 changed files with 1120 additions and 254 deletions
@@ -325,6 +325,7 @@ CREATE TABLE `portfolio_loan_repayment_schedule` (
`principal_completed_derived` decimal(19,6) DEFAULT NULL, `principal_completed_derived` decimal(19,6) DEFAULT NULL,
`interest_amount` decimal(19,6) DEFAULT NULL, `interest_amount` decimal(19,6) DEFAULT NULL,
`interest_completed_derived` decimal(19,6) DEFAULT NULL, `interest_completed_derived` decimal(19,6) DEFAULT NULL,
`interest_waived_derived` decimal(19,6) DEFAULT NULL,
`completed_derived` bit(1) NOT NULL, `completed_derived` bit(1) NOT NULL,
`createdby_id` bigint(20) DEFAULT NULL, `createdby_id` bigint(20) DEFAULT NULL,
`created_date` datetime DEFAULT NULL, `created_date` datetime DEFAULT NULL,
@@ -342,6 +343,8 @@ CREATE TABLE `portfolio_loan_transaction` (
`contra_id` bigint(20) DEFAULT NULL, `contra_id` bigint(20) DEFAULT NULL,
`transaction_date` date NOT NULL, `transaction_date` date NOT NULL,
`amount` decimal(19,6) NOT NULL, `amount` decimal(19,6) NOT NULL,
`principal_portion_derived` decimal(19,6) NOT NULL,
`interest_portion_derived` decimal(19,6) NOT NULL,
`createdby_id` bigint(20) DEFAULT NULL, `createdby_id` bigint(20) DEFAULT NULL,
`created_date` datetime DEFAULT NULL, `created_date` datetime DEFAULT NULL,
`lastmodified_date` datetime DEFAULT NULL, `lastmodified_date` datetime DEFAULT NULL,
@@ -8,7 +8,8 @@ VALUES
(300, 'Active'), (300, 'Active'),
(400, 'Withdrawn by client'), (400, 'Withdrawn by client'),
(500, 'Rejected'), (500, 'Rejected'),
(600, 'Closed'); (600, 'Closed'),
(700, 'Overpaid');
INSERT INTO `ref_currency` INSERT INTO `ref_currency`
@@ -0,0 +1,12 @@
-- add derived fields for loan transactions and extra
ALTER TABLE `portfolio_loan_transaction`
ADD COLUMN `principal_portion_derived` decimal(19,6) NOT NULL DEFAULT 0,
ADD COLUMN `interest_portion_derived` decimal(19,6) NOT NULL DEFAULT 0,
ADD COLUMN `interest_waived_derived` decimal(19,6) NOT NULL DEFAULT 0;
ALTER TABLE `portfolio_loan_repayment_schedule`
ADD COLUMN `interest_waived_derived` decimal(19,6) NOT NULL DEFAULT 0;
INSERT INTO `ref_loan_status` (`id`,`display_name`) VALUES (700, 'Overpaid');
@@ -1,6 +1,5 @@
package org.mifosng.platform.api.data; package org.mifosng.platform.api.data;
public class LoanAccountSummaryData { public class LoanAccountSummaryData {
private MoneyData originalPrincipal; private MoneyData originalPrincipal;
@@ -9,32 +8,42 @@ public class LoanAccountSummaryData {
private MoneyData principalPaid; private MoneyData principalPaid;
private MoneyData principalOutstanding; private MoneyData principalOutstanding;
private MoneyData interestPaid; private MoneyData interestPaid;
private MoneyData interestWaived;
private MoneyData interestOutstanding; private MoneyData interestOutstanding;
private MoneyData totalPaid; private MoneyData totalPaid;
private MoneyData totalWaived;
private MoneyData totalOutstanding; private MoneyData totalOutstanding;
private MoneyData totalInArrears; private MoneyData totalInArrears;
private MoneyData totalWaived;
protected LoanAccountSummaryData() { protected LoanAccountSummaryData() {
// //
} }
public LoanAccountSummaryData(MoneyData originalPrincipal, public LoanAccountSummaryData(
MoneyData principalPaid, MoneyData principalOutstanding, MoneyData originalPrincipal,
MoneyData originalInterest, MoneyData interestPaid, MoneyData principalPaid,
MoneyData interestOutstanding, MoneyData originalTotal, MoneyData principalOutstanding,
MoneyData totalPaid, MoneyData totalOutstanding, MoneyData totalInArrears, MoneyData totalWaived) { MoneyData originalInterest,
MoneyData interestPaid,
MoneyData interestWaived,
MoneyData interestOutstanding,
MoneyData originalTotal,
MoneyData totalPaid,
MoneyData totalWaived,
MoneyData totalOutstanding,
MoneyData totalInArrears) {
this.originalPrincipal = originalPrincipal; this.originalPrincipal = originalPrincipal;
this.principalPaid = principalPaid; this.principalPaid = principalPaid;
this.principalOutstanding = principalOutstanding; this.principalOutstanding = principalOutstanding;
this.originalInterest = originalInterest; this.originalInterest = originalInterest;
this.interestPaid = interestPaid; this.interestPaid = interestPaid;
this.interestWaived = interestWaived;
this.interestOutstanding = interestOutstanding; this.interestOutstanding = interestOutstanding;
this.originalTotal = originalTotal; this.originalTotal = originalTotal;
this.totalPaid = totalPaid; this.totalPaid = totalPaid;
this.totalWaived = totalWaived;
this.totalOutstanding = totalOutstanding; this.totalOutstanding = totalOutstanding;
this.totalInArrears = totalInArrears; this.totalInArrears = totalInArrears;
this.totalWaived = totalWaived;
} }
public boolean isWaiveAllowed(MoneyData tolerance) { public boolean isWaiveAllowed(MoneyData tolerance) {
@@ -88,6 +97,14 @@ public class LoanAccountSummaryData {
public void setInterestPaid(MoneyData interestPaid) { public void setInterestPaid(MoneyData interestPaid) {
this.interestPaid = interestPaid; this.interestPaid = interestPaid;
} }
public MoneyData getInterestWaived() {
return interestWaived;
}
public void setInterestWaived(MoneyData interestWaived) {
this.interestWaived = interestWaived;
}
public MoneyData getInterestOutstanding() { public MoneyData getInterestOutstanding() {
return interestOutstanding; return interestOutstanding;
@@ -104,6 +121,14 @@ public class LoanAccountSummaryData {
public void setTotalPaid(MoneyData totalPaid) { public void setTotalPaid(MoneyData totalPaid) {
this.totalPaid = totalPaid; this.totalPaid = totalPaid;
} }
public MoneyData getTotalWaived() {
return totalWaived;
}
public void setTotalWaived(MoneyData totalWaived) {
this.totalWaived = totalWaived;
}
public MoneyData getTotalOutstanding() { public MoneyData getTotalOutstanding() {
return totalOutstanding; return totalOutstanding;
@@ -120,12 +145,4 @@ public class LoanAccountSummaryData {
public void setTotalInArrears(MoneyData totalInArrears) { public void setTotalInArrears(MoneyData totalInArrears) {
this.totalInArrears = totalInArrears; this.totalInArrears = totalInArrears;
} }
public MoneyData getTotalWaived() {
return totalWaived;
}
public void setTotalWaived(MoneyData totalWaived) {
this.totalWaived = totalWaived;
}
} }
@@ -12,9 +12,11 @@ public class LoanRepaymentPeriodData {
private MoneyData principalOutstanding; private MoneyData principalOutstanding;
private MoneyData interest; private MoneyData interest;
private MoneyData interestPaid; private MoneyData interestPaid;
private MoneyData interestWaived;
private MoneyData interestOutstanding; private MoneyData interestOutstanding;
private MoneyData total; private MoneyData total;
private MoneyData totalPaid; private MoneyData totalPaid;
private MoneyData totalWaived;
private MoneyData totalOutstanding; private MoneyData totalOutstanding;
public LoanRepaymentPeriodData() { public LoanRepaymentPeriodData() {
@@ -24,8 +26,8 @@ public class LoanRepaymentPeriodData {
public LoanRepaymentPeriodData(Long loanId, Integer period, public LoanRepaymentPeriodData(Long loanId, Integer period,
LocalDate date, MoneyData principal, MoneyData principalPaid, LocalDate date, MoneyData principal, MoneyData principalPaid,
MoneyData principalOutstanding, MoneyData interest, MoneyData principalOutstanding, MoneyData interest,
MoneyData interestPaid, MoneyData interestOutstanding, MoneyData interestPaid, MoneyData interestWaived, MoneyData interestOutstanding,
MoneyData total, MoneyData totalPaid, MoneyData totalOutstanding) { MoneyData total, MoneyData totalPaid, MoneyData totalWaived, MoneyData totalOutstanding) {
this.loanId = loanId; this.loanId = loanId;
this.period = period; this.period = period;
this.date = date; this.date = date;
@@ -34,9 +36,11 @@ public class LoanRepaymentPeriodData {
this.principalOutstanding = principalOutstanding; this.principalOutstanding = principalOutstanding;
this.interest = interest; this.interest = interest;
this.interestPaid = interestPaid; this.interestPaid = interestPaid;
this.interestWaived = interestWaived;
this.interestOutstanding = interestOutstanding; this.interestOutstanding = interestOutstanding;
this.total = total; this.total = total;
this.totalPaid = totalPaid; this.totalPaid = totalPaid;
this.totalWaived = totalWaived;
this.totalOutstanding = totalOutstanding; this.totalOutstanding = totalOutstanding;
} }
@@ -103,6 +107,14 @@ public class LoanRepaymentPeriodData {
public void setInterestPaid(MoneyData interestPaid) { public void setInterestPaid(MoneyData interestPaid) {
this.interestPaid = interestPaid; this.interestPaid = interestPaid;
} }
public MoneyData getInterestWaived() {
return interestWaived;
}
public void setInterestWaived(MoneyData interestWaived) {
this.interestWaived = interestWaived;
}
public MoneyData getInterestOutstanding() { public MoneyData getInterestOutstanding() {
return interestOutstanding; return interestOutstanding;
@@ -127,6 +139,14 @@ public class LoanRepaymentPeriodData {
public void setTotalPaid(MoneyData totalPaid) { public void setTotalPaid(MoneyData totalPaid) {
this.totalPaid = totalPaid; this.totalPaid = totalPaid;
} }
public MoneyData getTotalWaived() {
return totalWaived;
}
public void setTotalWaived(MoneyData totalWaived) {
this.totalWaived = totalWaived;
}
public MoneyData getTotalOutstanding() { public MoneyData getTotalOutstanding() {
return totalOutstanding; return totalOutstanding;
@@ -54,7 +54,7 @@ public class DefaultLoanLifecycleStateMachine implements
} }
break; break;
case LOAN_REPAYMENT: case LOAN_REPAYMENT:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses); newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
} else { } else {
newState = from; newState = from;
@@ -80,6 +80,11 @@ public class DefaultLoanLifecycleStateMachine implements
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
} }
break; break;
case LOAN_OVERPAYMENT:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.OVERPAID, allowedLoanStatuses);
}
break;
} }
return newState; return newState;
@@ -0,0 +1,204 @@
package org.mifosng.platform.loan.domain;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
/**
* Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}.
*
* For standard transactions, pays off components in order of interest, then principal.
*
* If a transaction results in an overpayment for a given installment, the over paid amount is pay off on the principal component of subsequent installments.
*/
public class DefaultLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
@Override
public void handleTransaction(final List<LoanTransaction> repaymentsOrWaivers,
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
}
for (LoanTransaction loanTransaction : repaymentsOrWaivers) {
loanTransaction.resetDerivedComponents();
handleTransaction(loanTransaction, currency, installments);
}
}
@Override
public void handleTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency, final List<LoanRepaymentScheduleInstallment> installments) {
// find earliest unpaid installment for which to apply this transaction to.
boolean transactionProcessed = false;
int installmentIndex = 0;
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyCompleted() && !transactionProcessed) {
if (isTransactionInAdvance(installmentIndex, installments, loanTransaction)) {
handleTransactionInAdvance(installments, loanTransaction, currency);
transactionProcessed = true;
} else if (isTransactionLate(installmentIndex, installments, loanTransaction.getTransactionDate())) {
// does this result in a late payment of existing installment?
handleLateTransaction(installments, loanTransaction, currency);
transactionProcessed = true;
} else {
// standard transaction
Money installmentOverPaymentAmount = handleOnTimeTransaction(currentInstallment, loanTransaction, currency);
Money loanOverPaymentAmount = handleOverpaymentTransaction(installments, loanTransaction, currency, installmentOverPaymentAmount);
if (loanOverPaymentAmount.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, currency, loanOverPaymentAmount);
}
transactionProcessed = true;
}
}
installmentIndex++;
}
}
/**
* Dont do anything special for late payments e.g. no fees, charges or penalties are applied
*/
protected void handleLateTransaction(
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final MonetaryCurrency currency) {
final Money lateRepayment = loanTransaction.getAmount(currency);
Money loanOverPaymentAmount = handleOverpaymentTransaction(installments, loanTransaction, currency, lateRepayment);
if (loanOverPaymentAmount.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, currency, loanOverPaymentAmount);
}
}
/**
* just process transactions paid in advance the same as you would handle an overpayment.
*/
protected void handleTransactionInAdvance(
List<LoanRepaymentScheduleInstallment> installments,
LoanTransaction loanTransaction, MonetaryCurrency currency) {
final Money paymentInAdvance = loanTransaction.getAmount(currency);
Money loanOverPaymentAmount = handleOverpaymentTransaction(installments, loanTransaction, currency, paymentInAdvance);
if (loanOverPaymentAmount.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, currency, loanOverPaymentAmount);
}
}
/**
* called when a transaction results in a overpayment of the full loan i.e.
* transaction amount is greater than the total expected principal +
* interest of the loan.
*/
protected void onLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, @SuppressWarnings("unused") final MonetaryCurrency currency,
@SuppressWarnings("unused") final Money loanOverPaymentAmount) {
}
/**
* Handles overpayment scenario by paying of the principal of the next/subsequent installments that are not fully completed.
*/
protected Money handleOverpaymentTransaction(
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction, MonetaryCurrency currency,
final Money overPaymentAmount) {
Money transactionAmountRemaining = overPaymentAmount;
Money principalPortion = Money.zero(currency);
Money interestPortion = Money.zero(currency);
Money interestWaivedPortion = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = installment.payPrincipalComponent(transactionAmountRemaining);
if (installment.isPrincipalCompleted(currency)) {
interestWaivedPortion = installment.waiveInterestComponent(installment.getInterest(currency));
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
// reset portions for next installment
principalPortion = Money.zero(currency);
interestPortion = Money.zero(currency);
interestWaivedPortion = Money.zero(currency);
}
return transactionAmountRemaining;
}
/**
*
*/
protected Money handleOnTimeTransaction(final LoanRepaymentScheduleInstallment installment, final LoanTransaction loanTransaction, MonetaryCurrency currency) {
Money transactionAmountRemaining = loanTransaction.getAmount(currency);
Money interestPortion = installment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = installment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion);
return transactionAmountRemaining;
}
/**
* Is overpayment if the amount in the transactions is greater than amount due.
*/
protected boolean isTransactionOverpayment(final int installmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final Money transactionAmount) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installmentIndex);
return transactionAmount.isGreaterThan(currentInstallment.getTotalDue(transactionAmount.getCurrency()));
}
/**
* Is late if the date of the transactions is after that of the installment due date.
*/
protected boolean isTransactionLate(final int installmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installmentIndex);
return transactionDate.isAfter(currentInstallment.getDueDate());
}
/**
* is in advance if the transaction date is before or equal to last installment due date (in other words not after last installment date)
*/
protected boolean isTransactionInAdvance(final int installmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction transaction) {
boolean isInAdvance = false;
LocalDate lastInstallmentDueDate = null;
if (installmentIndex > 0) {
int previousInstallmentIndex = installmentIndex - 1;
LoanRepaymentScheduleInstallment previousInstallment = installments.get(previousInstallmentIndex);
lastInstallmentDueDate = previousInstallment.getDueDate();
return !transaction.getTransactionDate().isAfter(lastInstallmentDueDate);
}
return isInAdvance;
}
}
@@ -2,6 +2,7 @@ package org.mifosng.platform.loan.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -354,10 +355,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.disbursedOnDate = null; this.disbursedOnDate = null;
} }
public void waive(LoanTransaction loanTransaction, public void waive(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanLifecycleStateMachine loanLifecycleStateMachine) { this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.LOAN_REPAYMENT, this.loanStatus);
loanTransaction.updateLoan(this); loanTransaction.updateLoan(this);
this.loanTransactions.add(loanTransaction); this.loanTransactions.add(loanTransaction);
@@ -395,49 +394,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
waived, getInArrearsTolerance()); waived, getInArrearsTolerance());
} }
if (this.isRepaidInFull()) { doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
if (isInterestRebateAllowed()) {
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
.getTransactionDate());
if (rebateDue.isGreaterThanZero()) {
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
this.interestRebateOwed = rebateDue.getAmount();
}
}
}
} }
public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
handleRepaymentTransaction(loanTransaction, loanLifecycleStateMachine, false);
}
private void handleRepaymentTransaction(
final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final boolean adjusted) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
loanTransaction.updateLoan(this); loanTransaction.updateLoan(this);
boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, this.loanTransactions);
this.loanTransactions.add(loanTransaction); this.loanTransactions.add(loanTransaction);
deriveLoanRepaymentScheduleCompletedData();
// Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
// MonetaryCurrency currency = totalRepaidOrWaived.getCurrency();
//
// int repaymentIndex = 0;
//
// for (LoanTransaction transaction : this.loanTransactions) {
// if (transaction.isRepayment() || transaction.isWaiver()) {
// totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction.getAmount());
//
// LoanRepaymentScheduleInstallment currentInstallment = this.repaymentScheduleInstallments.get(repaymentIndex);
// if (currentInstallment.unpaid()) {
// currentInstallment.payOffWith(transaction, currency);
// } else {
// repaymentIndex++;
// }
// }
// }
if (loanTransaction.isNotRepayment()) { if (loanTransaction.isNotRepayment()) {
final String errorMessage = "A transaction of type repayment was expected but not received."; final String errorMessage = "A transaction of type repayment was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", throw new InvalidLoanTransactionTypeException("transaction",
@@ -458,66 +431,81 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
throw new InvalidLoanStateTransitionException("repayment", throw new InvalidLoanStateTransitionException("repayment",
"cannot.be.a.futre.date", errorMessage, loanTransactionDate); "cannot.be.a.futre.date", errorMessage, loanTransactionDate);
} }
List<LoanTransaction> repaymentsOrWaivers = new ArrayList<LoanTransaction>();
for (LoanTransaction transaction : this.loanTransactions) {
if (!transaction.isDisbursement() && transaction.isNotContra()) {
repaymentsOrWaivers.add(transaction);
}
}
LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(repaymentsOrWaivers, transactionComparator);
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = new DefaultLoanRepaymentScheduleTransactionProcessor();
if (isTransactionChronologicallyLatest && !adjusted) {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments);
} else {
loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments);
}
doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
}
private void doPostLoanTransactionChecks(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (this.isOverPaid()) {
handleLoanOverpayment(loanTransaction, loanLifecycleStateMachine);
}
if (this.isRepaidInFull()) { if (this.isRepaidInFull()) {
this.loanStatus = loanLifecycleStateMachine.transition( handleLoanRepaymentInFull(loanTransaction, loanLifecycleStateMachine);
LoanEvent.REPAID_IN_FULL, this.loanStatus); }
this.closedOnDate = loanTransaction.getTransactionDate().toDate(); }
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
if (isInterestRebateAllowed()) { private void handleLoanRepaymentInFull(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus);
.getTransactionDate()); this.closedOnDate = loanTransaction.getTransactionDate().toDate();
if (rebateDue.isGreaterThanZero()) { this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.INTERST_REBATE_OWED, this.loanStatus); if (isInterestRebateAllowed()) {
this.interestRebateOwed = rebateDue.getAmount(); Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction.getTransactionDate());
if (rebateDue.isGreaterThanZero()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
this.interestRebateOwed = rebateDue.getAmount();
}
}
}
private void handleLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, this.loanStatus);
this.closedOnDate = null;
this.maturedOnDate = null;
}
private boolean isChronologicallyLatestRepaymentOrWaiver(
final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (LoanTransaction previousTransaction : loanTransactions) {
if (!previousTransaction.isDisbursement() && previousTransaction.isNotContra()) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate()) ||
currentTransactionDate.isEqual(previousTransaction.getTransactionDate())) {
isChronologicallyLatestRepaymentOrWaiver = false;
break;
} }
} }
} }
}
return isChronologicallyLatestRepaymentOrWaiver;
private void deriveLoanRepaymentScheduleCompletedData() {
Money totalRepaidOrWaivedAgainstLoan = calculateTotalPaidOrWaived();
Money remainingToPayoffAgainstLoanSchedule = totalRepaidOrWaivedAgainstLoan;
Money totalOverpaid = Money.zero(totalRepaidOrWaivedAgainstLoan.getCurrency());
int repaymentInstallmentIndex = 0;
while (remainingToPayoffAgainstLoanSchedule.isGreaterThanZero()) {
if (repaymentInstallmentIndex == this.repaymentScheduleInstallments.size()) {
totalOverpaid = remainingToPayoffAgainstLoanSchedule;
// to exit while loop
remainingToPayoffAgainstLoanSchedule = remainingToPayoffAgainstLoanSchedule.minus(totalOverpaid);
} else {
LoanRepaymentScheduleInstallment scheduledRepaymentInstallment = this.repaymentScheduleInstallments.get(repaymentInstallmentIndex);
remainingToPayoffAgainstLoanSchedule = scheduledRepaymentInstallment.updateDerivedComponents(remainingToPayoffAgainstLoanSchedule);
repaymentInstallmentIndex++;
}
}
}
private Money calculateTotalPaidOrWaived() {
Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepayment() || transaction.isWaiver()) {
totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction
.getAmount());
}
}
return totalRepaidOrWaived;
} }
public LocalDate possibleNextRepaymentDate() { public LocalDate possibleNextRepaymentDate() {
LocalDate earliestUnpaidInstallmentDate = new LocalDate(); LocalDate earliestUnpaidInstallmentDate = new LocalDate();
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) { if (installment.isNotFullyCompleted()) {
earliestUnpaidInstallmentDate = installment.getDueDate(); earliestUnpaidInstallmentDate = installment.getDueDate();
break; break;
} }
@@ -545,7 +533,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
Money possibleNextRepaymentAmount = Money.zero(currency); Money possibleNextRepaymentAmount = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) { if (installment.isNotFullyCompleted()) {
possibleNextRepaymentAmount = installment.getTotalDue(currency); possibleNextRepaymentAmount = installment.getTotalDue(currency);
break; break;
} }
@@ -568,7 +556,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
transactionForAdjustment.contra(); transactionForAdjustment.contra();
if (newTransactionDetail.isRepayment()) { if (newTransactionDetail.isRepayment()) {
makeRepayment(newTransactionDetail, loanLifecycleStateMachine); handleRepaymentTransaction(newTransactionDetail, loanLifecycleStateMachine, true);
} }
if (newTransactionDetail.isWaiver()) { if (newTransactionDetail.isWaiver()) {
@@ -577,39 +565,42 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} }
private boolean isRepaidInFull() { private boolean isRepaidInFull() {
MonetaryCurrency currency = loanCurrency();
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail Money cumulativeOriginalExpectedTotal = Money.zero(currency);
.getPrincipal().getCurrency()); Money cumulativeTotalPaid = Money.zero(currency);
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail Money cumulativeTotalWaived = Money.zero(currency);
.getPrincipal().getCurrency());
Money cumulativeTotal = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeWaived = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency));
.getPrincipal(loanCurrency())); cumulativeTotalPaid = cumulativeTotalPaid.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency)));
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment cumulativeTotalWaived = cumulativeTotalWaived.plus(scheduledRepayment.getInterestWaived(currency));
.getInterest(loanCurrency()));
cumulativeTotal = cumulativeTotal.plus(scheduledRepayment
.getTotal(loanCurrency()));
} }
Money totalOutstanding = cumulativeOriginalExpectedTotal.minus(cumulativeTotalPaid.plus(cumulativeTotalWaived));
boolean isRepaidInFull = totalOutstanding.isZero();
return isRepaidInFull;
}
private boolean isOverPaid() {
Money totalPaidInRepayments = getTotalPaidInRepayments();
MonetaryCurrency currency = loanCurrency();
for (LoanTransaction transaction : this.loanTransactions) { Money cumulativeOriginalExpectedTotal = Money.zero(currency);
if (transaction.isRepayment()) { Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
cumulativePaid = cumulativePaid.plus(transaction.getAmount()); Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
}
if (transaction.isWaiver()) { for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeWaived = cumulativeWaived.plus(transaction cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency));
.getAmount()); cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency)));
} cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
} }
return cumulativePaid.plus(cumulativeWaived).isGreaterThanOrEqualTo( return totalPaidInRepayments.isGreaterThan(cumulativeTotalPaidOnInstallments);
cumulativeTotal);
} }
private MonetaryCurrency loanCurrency() { private MonetaryCurrency loanCurrency() {
@@ -914,7 +905,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate); LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate);
Money totalPaidToDate = this.getTotalPaid(); Money totalPaidToDate = this.getTotalPaidInRepayments();
Money totalOutstandingBasedOnExpectedMaturityDate = this Money totalOutstandingBasedOnExpectedMaturityDate = this
.getTotalOutstanding(); .getTotalOutstanding();
@@ -937,13 +928,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} }
public Money getTotalOutstanding() { public Money getTotalOutstanding() {
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus( return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(getTotalPaidInRepayments());
getTotalPaid());
} }
private Money getTotalPaid() { private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
.getPrincipal().getCurrency());
for (LoanTransaction repayment : this.loanTransactions) { for (LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepayment()) { if (repayment.isRepayment()) {
@@ -4,5 +4,5 @@ public enum LoanEvent {
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT, LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT,
// //
REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED; REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED, LOAN_OVERPAYMENT;
} }
@@ -29,16 +29,19 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
private final Integer installmentNumber; private final Integer installmentNumber;
@Column(name = "principal_amount", scale=6, precision=19) @Column(name = "principal_amount", scale=6, precision=19)
private BigDecimal principal; private BigDecimal principal = BigDecimal.ZERO;
@Column(name = "interest_amount", scale=6, precision=19) @Column(name = "interest_amount", scale=6, precision=19)
private BigDecimal interest; private BigDecimal interest = BigDecimal.ZERO;
@Column(name = "principal_completed_derived", scale=6, precision=19) @Column(name = "principal_completed_derived", scale=6, precision=19)
private BigDecimal principalCompleted; private BigDecimal principalCompleted = BigDecimal.ZERO;
@Column(name = "interest_completed_derived", scale = 6, precision = 19) @Column(name = "interest_completed_derived", scale = 6, precision = 19)
private BigDecimal interestCompleted; private BigDecimal interestCompleted = BigDecimal.ZERO;
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
private BigDecimal interestWaived = BigDecimal.ZERO;
@Column(name="completed_derived") @Column(name="completed_derived")
private boolean completed; private boolean completed;
@@ -82,40 +85,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return new LocalDate(this.dueDate); return new LocalDate(this.dueDate);
} }
public Money updateDerivedComponents(Money totalAvailable) {
Money remaining = totalAvailable.copy();
boolean principalCompletedInFull = false;
boolean interestCompletedInFull = false;
Money interest = getInterest(totalAvailable.getCurrency());
Money principal = getPrincipal(totalAvailable.getCurrency());
// TODO - NEWFEATURE - configuration around order components of loan are paid off in.
// pay off interest
if (remaining.isGreaterThanOrEqualTo(interest)) {
this.interestCompleted = interest.getAmount();
interestCompletedInFull = true;
} else {
this.interestCompleted = remaining.getAmount();
}
remaining = remaining.minus(this.interestCompleted);
// pay off principal
if (remaining.isGreaterThanOrEqualTo(principal)) {
this.principalCompleted = principal.getAmount();
principalCompletedInFull = true;
} else {
this.principalCompleted = remaining.getAmount();
}
remaining = remaining.minus(this.principalCompleted);
this.completed = (principalCompletedInFull && interestCompletedInFull);
return remaining;
}
public Money getPrincipal(MonetaryCurrency currency) { public Money getPrincipal(MonetaryCurrency currency) {
return Money.of(currency, this.principal); return Money.of(currency, this.principal);
} }
@@ -132,6 +101,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return Money.of(currency, this.interestCompleted); return Money.of(currency, this.interestCompleted);
} }
public Money getInterestWaived(MonetaryCurrency currency) {
return Money.of(currency, this.interestWaived);
}
public Money getTotal(MonetaryCurrency currency) { public Money getTotal(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterest(currency)); return getPrincipal(currency).plus(getInterest(currency));
} }
@@ -139,30 +112,96 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
public void updateLoan(final Loan loan) { public void updateLoan(final Loan loan) {
this.loan = loan; this.loan = loan;
} }
public boolean isFullyCompleted() {
return this.completed;
}
public boolean unpaid() { public boolean isNotFullyCompleted() {
return !this.completed; return !this.completed;
} }
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
return !isPrincipalCompleted(currency);
}
public boolean isPrincipalCompleted(final MonetaryCurrency currency) {
return getPrincipal(currency).minus(getPrincipalCompleted(currency)).isZero();
}
public Money getTotalDue(MonetaryCurrency currency) { public Money getTotalDue(MonetaryCurrency currency) {
return getTotal(currency).minus(getTotalCompleted(currency)); final Money totalPaidOrWaived = getInterestWaived(currency).plus(getTotalCompleted(currency));
return getTotal(currency).minus(totalPaidOrWaived);
} }
private Money getTotalCompleted(MonetaryCurrency currency) { private Money getTotalCompleted(MonetaryCurrency currency) {
return getPrincipalCompleted(currency).plus(getInterestCompleted(currency)); return getPrincipalCompleted(currency).plus(getInterestCompleted(currency));
} }
// public void payOffWith(final LoanTransaction transaction, final MonetaryCurrency currency) { public void resetDerivedComponents() {
// Money transactionAmountRemaining = Money.of(currency, transaction.getAmount()); this.principalCompleted = BigDecimal.ZERO;
// this.interestCompleted = BigDecimal.ZERO;
// Money interestDue = getInterest(currency); this.interestWaived = BigDecimal.ZERO;
// if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) { this.completed = false;
// this.interestCompleted = interestDue.getAmount(); }
// transactionAmountRemaining = transactionAmountRemaining.minus(interestDue);
// } else { public Money payInterestComponent(final Money transactionAmountRemaining) {
// this.interestCompleted = transactionAmountRemaining.getAmount();
// } MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
//
// Money principalDue = getPrincipal(currency); Money interestPortionOfTransaction = Money.zero(currency);
// }
Money interestExpected = getInterest(currency);
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
this.interestCompleted = getInterestCompleted(currency).plus(interestDue).getAmount();
interestPortionOfTransaction = interestPortionOfTransaction.plus(interestDue);
} else {
this.interestCompleted = getInterestCompleted(currency).plus(transactionAmountRemaining).getAmount();
interestPortionOfTransaction = interestPortionOfTransaction.plus(transactionAmountRemaining);
}
this.completed = getTotalDue(currency).isZero();
return interestPortionOfTransaction;
}
public Money payPrincipalComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money principalPortionOfTransaction = Money.zero(currency);
Money principalExpected = getPrincipal(currency);
Money principalDue = principalExpected.minus(getPrincipalCompleted(currency));
if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) {
this.principalCompleted = getPrincipalCompleted(currency).plus(principalDue).getAmount();
principalPortionOfTransaction = principalPortionOfTransaction.plus(principalDue);
} else {
this.principalCompleted = getPrincipalCompleted(currency).plus(transactionAmountRemaining).getAmount();
principalPortionOfTransaction = principalPortionOfTransaction.plus(transactionAmountRemaining);
}
this.completed = getTotalDue(currency).isZero();
return principalPortionOfTransaction;
}
public Money waiveInterestComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedInterestPortionOfTransaction = Money.zero(currency);
Money interestExpected = getInterest(currency);
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
this.interestWaived = getInterestWaived(currency).plus(interestDue).getAmount();
waivedInterestPortionOfTransaction = waivedInterestPortionOfTransaction.plus(interestDue);
} else {
this.interestWaived = getInterestWaived(currency).plus(transactionAmountRemaining).getAmount();
waivedInterestPortionOfTransaction = waivedInterestPortionOfTransaction.plus(transactionAmountRemaining);
}
this.completed = getTotalDue(currency).isZero();
return waivedInterestPortionOfTransaction;
}
} }
@@ -0,0 +1,13 @@
package org.mifosng.platform.loan.domain;
import java.util.List;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
public interface LoanRepaymentScheduleTransactionProcessor {
void handleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments);
void handleTransaction(List<LoanTransaction> repaymentsOrWaivers, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
}
@@ -17,6 +17,7 @@ public class LoanStatus implements Persistable<Integer> {
public static final Integer WITHDRAWN_BY_CLIENT = 400; public static final Integer WITHDRAWN_BY_CLIENT = 400;
public static final Integer REJECTED = 500; public static final Integer REJECTED = 500;
public static final Integer CLOSED = 600; public static final Integer CLOSED = 600;
public static final Integer OVERPAID = 700;
@Id @Id
@Column(name = "id") @Column(name = "id")
@@ -37,6 +37,15 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "amount", scale = 6, precision = 19, nullable = false) @Column(name = "amount", scale = 6, precision = 19, nullable = false)
private final BigDecimal amount; private final BigDecimal amount;
@Column(name = "principal_portion_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal principalPortion = BigDecimal.ZERO;
@Column(name = "interest_portion_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal interestPortion = BigDecimal.ZERO;
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
private BigDecimal interestWaivedPortion = BigDecimal.ZERO;
@Temporal(TemporalType.DATE) @Temporal(TemporalType.DATE)
@Column(name = "transaction_date", nullable=false) @Column(name = "transaction_date", nullable=false)
private final Date dateOf; private final Date dateOf;
@@ -155,4 +164,36 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
public boolean isNonZero() { public boolean isNonZero() {
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0; return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
} }
/**
* This updates the derived fields of a loan transaction for the principal, interest and interest waived portions.
*
* This accumulates the values passed to the already existent values for each of the portions.
*/
public void updateComponents(final Money principal, final Money interest, final Money interestWaivedPortion) {
MonetaryCurrency currency = principal.getCurrency();
this.principalPortion = getPrincipalPortion(currency).plus(principal).getAmount();
this.interestPortion = getInterestPortion(currency).plus(interest).getAmount();
this.interestWaivedPortion = getInterestWaivedPortion(currency).plus(interestWaivedPortion).getAmount();
}
public Money getPrincipalPortion(MonetaryCurrency currency) {
return Money.of(currency, principalPortion);
}
public Money getInterestPortion(MonetaryCurrency currency) {
return Money.of(currency, interestPortion);
}
public Money getInterestWaivedPortion(MonetaryCurrency currency) {
if (this.interestWaivedPortion == null) {
this.interestWaivedPortion = BigDecimal.ZERO;
}
return Money.of(currency, interestWaivedPortion);
}
public void resetDerivedComponents() {
this.principalPortion = BigDecimal.ZERO;
this.interestPortion = BigDecimal.ZERO;
}
} }
@@ -0,0 +1,15 @@
package org.mifosng.platform.loan.domain;
import java.util.Comparator;
/**
*
*/
public class LoanTransactionComparator implements Comparator<LoanTransaction> {
@Override
public int compare(final LoanTransaction o1, final LoanTransaction o2) {
return o1.getTransactionDate().compareTo(o2.getTransactionDate());
}
}
@@ -0,0 +1,22 @@
package org.mifosng.platform.loan.domain;
import org.mifosng.platform.currency.domain.Money;
public class TransactionProccessingResult {
private final Money overPaymentAmount;
private final boolean overPayment;
public TransactionProccessingResult(final Money overPaymentAmount, final boolean overPayment) {
this.overPaymentAmount = overPaymentAmount;
this.overPayment = overPayment;
}
public Money getOverPaymentAmount() {
return overPaymentAmount;
}
public boolean isOverPayment() {
return overPayment;
}
}
@@ -122,60 +122,66 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
BigDecimal principalOutstanding = BigDecimal.ZERO; BigDecimal principalOutstanding = BigDecimal.ZERO;
BigDecimal originalInterest = BigDecimal.ZERO; BigDecimal originalInterest = BigDecimal.ZERO;
BigDecimal interestPaid = BigDecimal.ZERO; BigDecimal interestPaid = BigDecimal.ZERO;
BigDecimal interestWaived = BigDecimal.ZERO;
BigDecimal interestOutstanding = BigDecimal.ZERO; BigDecimal interestOutstanding = BigDecimal.ZERO;
BigDecimal originalTotal = BigDecimal.ZERO; BigDecimal originalTotal = BigDecimal.ZERO;
BigDecimal totalPaid = BigDecimal.ZERO; BigDecimal totalPaid = BigDecimal.ZERO;
BigDecimal totalOutstanding = BigDecimal.ZERO;
BigDecimal totalInArrears = BigDecimal.ZERO;
BigDecimal totalWaived = BigDecimal.ZERO; BigDecimal totalWaived = BigDecimal.ZERO;
BigDecimal totalOutstanding = BigDecimal.ZERO;
BigDecimal totalInArrears = BigDecimal.ZERO;
for (LoanRepaymentPeriodData installment : repaymentSchedule) { for (LoanRepaymentPeriodData installment : repaymentSchedule) {
originalPrincipal = originalPrincipal.add(installment originalPrincipal = originalPrincipal.add(installment.getPrincipal().getAmount());
.getPrincipal().getAmount()); principalPaid = principalPaid.add(installment.getPrincipalPaid().getAmount());
principalPaid = principalPaid.add(installment.getPrincipalPaid() principalOutstanding = principalOutstanding.add(installment.getPrincipalOutstanding().getAmount());
.getAmount());
principalOutstanding = principalOutstanding.add(installment
.getPrincipalOutstanding().getAmount());
originalInterest = originalInterest.add(installment.getInterest() originalInterest = originalInterest.add(installment.getInterest().getAmount());
.getAmount()); interestPaid = interestPaid.add(installment.getInterestPaid().getAmount());
interestPaid = interestPaid.add(installment.getInterestPaid() interestWaived = interestWaived.add(installment.getInterestWaived().getAmount());
.getAmount()); interestOutstanding = interestOutstanding.add(installment.getInterestOutstanding().getAmount());
interestOutstanding = interestOutstanding.add(installment
.getInterestOutstanding().getAmount());
originalTotal = originalTotal.add(installment.getTotal() originalTotal = originalTotal.add(installment.getTotal().getAmount());
.getAmount());
totalPaid = totalPaid.add(installment.getTotalPaid().getAmount()); totalPaid = totalPaid.add(installment.getTotalPaid().getAmount());
totalOutstanding = totalOutstanding.add(installment totalWaived = totalWaived.add(installment.getTotalWaived().getAmount());
.getTotalOutstanding().getAmount()); totalOutstanding = totalOutstanding.add(installment.getTotalOutstanding().getAmount());
if (installment.getDate().isBefore(new LocalDate())) { if (installment.getDate().isBefore(new LocalDate())) {
totalInArrears = totalInArrears.add(installment totalInArrears = totalInArrears.add(installment.getTotalOutstanding().getAmount());
.getTotalOutstanding().getAmount());
} }
} }
// sum up all repayment transactions
Long waiverType = (long) 4; Long waiverType = (long) 4;
MoneyData totalRepaymentAmount = MoneyData.of(currencyData, BigDecimal.ZERO);
for (LoanRepaymentTransactionData loanRepayment : loanRepayments) { for (LoanRepaymentTransactionData loanRepayment : loanRepayments) {
Long transactionType = loanRepayment.getTransactionType().getId(); Long transactionType = loanRepayment.getTransactionType().getId();
if (transactionType.equals(waiverType)) { if (transactionType.equals(waiverType)) {
totalWaived = totalWaived.add(loanRepayment.getTotal() // skip
.getAmount()); } else {
totalRepaymentAmount = totalRepaymentAmount.plus(loanRepayment.getTotal());
} }
}
// detect if loan is in overpaid state
if (totalRepaymentAmount.isGreaterThan(MoneyData.of(currencyData, totalPaid))) {
BigDecimal difference = totalRepaymentAmount.getAmount().subtract(totalPaid);
totalOutstanding = totalOutstanding.subtract(difference);
} }
return new LoanAccountSummaryData(MoneyData.of(currencyData, return new LoanAccountSummaryData(
originalPrincipal), MoneyData.of(currencyData, principalPaid), MoneyData.of(currencyData,originalPrincipal),
MoneyData.of(currencyData, principalOutstanding), MoneyData.of( MoneyData.of(currencyData, principalPaid),
currencyData, originalInterest), MoneyData.of( MoneyData.of(currencyData, principalOutstanding),
currencyData, interestPaid), MoneyData.of(currencyData, MoneyData.of(currencyData, originalInterest),
interestOutstanding), MoneyData.of(currencyData, MoneyData.of(currencyData, interestPaid),
originalTotal), MoneyData.of(currencyData, totalPaid), MoneyData.of(currencyData, interestWaived),
MoneyData.of(currencyData, totalOutstanding), MoneyData.of( MoneyData.of(currencyData, interestOutstanding),
currencyData, totalInArrears), MoneyData.of( MoneyData.of(currencyData, originalTotal),
currencyData, totalWaived)); MoneyData.of(currencyData, totalPaid),
MoneyData.of(currencyData, totalWaived),
MoneyData.of(currencyData, totalOutstanding),
MoneyData.of(currencyData, totalInArrears));
} }
@Override @Override
@@ -524,7 +530,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
return " ls.loan_id as loanId, ls.installment as period, ls.duedate as `date`, " return " ls.loan_id as loanId, ls.installment as period, ls.duedate as `date`, "
+ " ls.principal_amount as principal, ls.principal_completed_derived as principalPaid, " + " ls.principal_amount as principal, ls.principal_completed_derived as principalPaid, "
+ " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, " + " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, "
+ " l.currency_code as currencyCode, l.currency_digits as currencyDigits, rc.`name` as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode " + " l.currency_code as currencyCode, l.currency_digits as currencyDigits, rc.`name` as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode "
+ " from portfolio_loan l " + " from portfolio_loan l "
+ " join portfolio_loan_repayment_schedule ls on ls.loan_id = l.id " + " join portfolio_loan_repayment_schedule ls on ls.loan_id = l.id "
@@ -553,29 +559,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
BigDecimal principalBD = rs.getBigDecimal("principal"); BigDecimal principalBD = rs.getBigDecimal("principal");
MoneyData principal = MoneyData.of(currencyData, principalBD); MoneyData principal = MoneyData.of(currencyData, principalBD);
BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid"); BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid");
MoneyData principalPaid = MoneyData.of(currencyData, MoneyData principalPaid = MoneyData.of(currencyData, principalPaidBD);
principalPaidBD); MoneyData principalOutstanding = MoneyData.of(currencyData,principalBD.subtract(principalPaidBD));
MoneyData principalOutstanding = MoneyData.of(currencyData,
principalBD.subtract(principalPaidBD));
BigDecimal interestBD = rs.getBigDecimal("interest"); BigDecimal interestBD = rs.getBigDecimal("interest");
MoneyData interest = MoneyData.of(currencyData, interestBD); MoneyData interest = MoneyData.of(currencyData, interestBD);
BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid"); BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid");
MoneyData interestPaid = MoneyData.of(currencyData, interestPaidBD); MoneyData interestPaid = MoneyData.of(currencyData, interestPaidBD);
MoneyData interestOutstanding = MoneyData.of(currencyData,
interestBD.subtract(interestPaidBD)); BigDecimal interestWaivedBD = rs.getBigDecimal("interestWaived");
if (interestWaivedBD == null) {
interestWaivedBD = BigDecimal.ZERO;
}
MoneyData interestWaived = MoneyData.of(currencyData, interestWaivedBD);
MoneyData interestOutstanding = MoneyData.of(currencyData, interestBD.subtract(interestPaidBD).subtract(interestWaivedBD));
MoneyData total = MoneyData.of(currencyData, MoneyData total = MoneyData.of(currencyData,principalBD.add(interestBD));
principalBD.add(interestBD)); MoneyData totalPaid = principalPaid.plus(interestPaid);
MoneyData totalPaid = MoneyData.of(currencyData, MoneyData totalWaived = interestWaived;
principalPaidBD.add(interestPaidBD)); MoneyData totalOutstanding = principalOutstanding.plus(interestOutstanding);
MoneyData totalOutstanding = MoneyData.of(currencyData,
principalBD.add(interestBD).subtract(principalPaidBD)
.subtract(interestPaidBD));
return new LoanRepaymentPeriodData(loanId, period, date, return new LoanRepaymentPeriodData(loanId, period, date,
principal, principalPaid, principalOutstanding, interest, principal, principalPaid, principalOutstanding, interest,
interestPaid, interestOutstanding, total, totalPaid, interestPaid, interestWaived, interestOutstanding, total, totalPaid, totalWaived,
totalOutstanding); totalOutstanding);
} }
} }
@@ -0,0 +1,392 @@
package org.mifosng.platform.loan.domain;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.List;
import org.joda.time.LocalDate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class DefaultLoanRepaymentScheduleTransactionProcessorTest {
// class under test
private DefaultLoanRepaymentScheduleTransactionProcessor processor;
//
private LocalDate july2nd = new LocalDate(2012, 7, 2);
private MonetaryCurrency usDollars = new MonetaryCurrencyBuilder().withCode("USD").withDigitsAfterDecimal(2).build();
private List<LoanRepaymentScheduleInstallment> installments;
@Before
public void setUpForEachTestCase() {
installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars);
processor = new DefaultLoanRepaymentScheduleTransactionProcessor();
}
/**
* Scenario 1: Single transaction which is less than the interest component of the first installment.
*
* Expectation:
* - First installment shows interest completed equal to that of transactions, zero principal completed. (payment order interest, principal)
* - transaction has interest portion equal to transaction amount, and zero principal portion
*/
@Test
public void givenSingleOnTimeLoanTransactionShouldPayoffInterestComponentFirst() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money oneHundredDollars = new MoneyBuilder().with(usDollars).with("100.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(oneHundredDollars).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", onTimePartialPayment);
}
/**
* Scenario 2: Two transactions which is less than the interest component of the first installment.
*
* Expectation:
* - First installment shows interest completed equal to that of transactions, zero principal completed. (payment order interest, principal)
* - transactions has interest portion equal to transaction amount, and zero principal portion
*/
@Test
public void givenMultipleOnTimeLoanTransactionShouldPayoffInterestComponentFirst() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money oneHundredDollars = new MoneyBuilder().with(usDollars).with("100.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(oneHundredDollars).build();
Money fiftyDollars = new MoneyBuilder().with(usDollars).with("50.00").build();
LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(fiftyDollars).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("0.00", "150.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", onTimePartialPayment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "50.00", secondOnTimePartialPayment);
}
/**
* Scenario 3: Single transaction which is greater than the interest component of the first installment but less than the total due for installment.
*
* Expectation:
* - First installment shows interest completed equal to that interest due, remainder is against principal completed. (payment order interest, principal)
* - transaction has interest portion equal to interest expected and principal portion of remainder.
*/
@Test
public void givenSingleOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponents() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money amount = new MoneyBuilder().with(usDollars).with("300.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", onTimePartialPayment);
}
/**
* Scenario 4: Single transaction which is greater than the interest component of the first installment but less than the total due for installment.
*
* Expectation:
* - First installment shows interest completed equal to that interest due, remainder is against principal completed. (payment order interest, principal)
* - transaction has interest portion equal to interest expected and principal portion of remainder.
*/
@Test
public void givenMultipleOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponents() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money amount = new MoneyBuilder().with(usDollars).with("300.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("50.00").build();
LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("150.00", "200.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", onTimePartialPayment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("50.00", "0.00", secondOnTimePartialPayment);
}
/**
* Scenario 5:Single transaction which is equal to the total due for installment.
*
* Expectation:
* - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal)
* - transaction has interest portion and principal portions
*/
@Test
public void givenSingleFullOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponentInFull() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money amount = new MoneyBuilder().with(usDollars).with("1200.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", onTimePartialPayment);
}
/**
* Scenario 6: Multiple transactions which together equal to the total due for installment.
*
* Expectation:
* - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal)
* - transaction has interest portion and principal portions
*/
@Test
public void givenMuliptleOnTimeLoanTransactionsThatInFullShouldPayoffInterestThenPrincipalComponentInFull() {
// setup
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
Money amount = new MoneyBuilder().with(usDollars).with("600.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("600.00").build();
LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("400.00", "200.00", onTimePartialPayment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("600.00", "0.00", secondOnTimePartialPayment);
}
/**
* Scenario 7: Single transaction which is greater than total due for first installment.
*
* Expectation:
* - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal)
* - Second installment shows principal completed equal to the remaining amount
* - transaction has interest portion and principal portions, the principal portion is the sum of principal completed for first and second installments
*/
@Test
public void givenSingleOnTimeLoanTransactionThatOverpaysInstallmentShouldPayoffInterestThenPrincipalComponentInFullOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfNextInstallment() {
// setup
Money amount = new MoneyBuilder().with(usDollars).with("1300.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", firstInstallment);
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "0.00", secondInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("1100.00", "200.00", onTimePartialPayment);
}
/**
* Scenario 8: Multiple transactions which together are greater than total due for first installment.
*
* Expectation:
* - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal)
* - Second installment shows principal completed equal to the remaining amount
*/
@Test
public void givenMultipleOnTimeLoanTransactionThatOverpaysInstallmentShouldPayoffInterestThenPrincipalComponentInFullOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfNextInstallment() {
// setup
Money amount = new MoneyBuilder().with(usDollars).with("600.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("700.00").build();
LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment);
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "0.00", secondInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("400.00", "200.00", onTimePartialPayment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "0.00", secondOnTimePartialPayment);
}
/**
* Scenario 9: Single transaction which overpays first installment and second installment with outstanding amount been account as principal in third installment.
*
* Where an installments principal is paid for fully in advance - the interest component is waived.
*/
@Test
public void givenOverPaymentShouldPayoffInterestThenPrincipalComponentsOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfAllSubsequentInstallments() {
// setup
Money amount = new MoneyBuilder().with(usDollars).with("3000.00").build();
LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build();
// execute test
processor.handleTransaction(onTimePartialPayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("0.00"), interestWaivedOf("200.00"), secondInstallment, usDollars);
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("800.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("2800.00"), interestOf("200.00"), interestWaivedOf("200.00"), onTimePartialPayment, usDollars);
}
/**
* Scenario 10: Single transaction which is a repayment in advance
*/
@Test
public void givenRepaymentOfInstallmentInAdvanceShouldPayoffOnlyPrincipalComponent() {
// setup
installments = LoanScheduleTestDataHelper.createSimpleLoanScheduleWithFirstInstallmentFullyPaid(july2nd, usDollars);
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
LocalDate paymentInAdvanceDate = firstInstallment.getDueDate().minusDays(1);
Money amount = new MoneyBuilder().with(usDollars).with("900.00").build();
LoanTransaction paymentInAdvance = new LoanTransactionBuilder().repayment().with(paymentInAdvanceDate).with(amount).build();
// execute test
processor.handleTransaction(paymentInAdvance, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("900.00", "0.00", secondInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("900.00", "0.00", paymentInAdvance);
}
/**
* Scenario 11: Single transaction which is a repayment many days early but still does not qualify for 'in advance'.
*/
@Test
public void givenRepaymentOfInstallmentBeforeDueDateButNotInAdvanceShouldPayoffInterestAndPrincipalComponents() {
// setup
installments = LoanScheduleTestDataHelper.createSimpleLoanScheduleWithFirstInstallmentFullyPaid(july2nd, usDollars);
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
LocalDate paymentBeforeDueDateButNotInAdvanceDate = firstInstallment.getDueDate().plusDays(1);
Money amount = new MoneyBuilder().with(usDollars).with("900.00").build();
LoanTransaction paymentBeforeDueDateButNotInAdvance = new LoanTransactionBuilder().repayment().with(paymentBeforeDueDateButNotInAdvanceDate).with(amount).build();
// execute test
processor.handleTransaction(paymentBeforeDueDateButNotInAdvance, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", secondInstallment);
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", paymentBeforeDueDateButNotInAdvance);
}
private void assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf(
final String principal, final String interest, final LoanTransaction transaction) {
Money expectedInterestPortion = new MoneyBuilder().with(usDollars).with(interest).build();
assertThat(transaction.getInterestPortion(usDollars).toString(), is(expectedInterestPortion.toString()));
Money expectedPrincipalPortion = new MoneyBuilder().with(usDollars).with(principal).build();
assertThat(transaction.getPrincipalPortion(usDollars).toString(), is(expectedPrincipalPortion.toString()));
}
private void assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf(final String principal, final String interest,
final LoanRepaymentScheduleInstallment installment) {
Money expectedInterestCompleted = new MoneyBuilder().with(usDollars).with(interest).build();
assertThat(installment.getInterestCompleted(usDollars).toString(), is(expectedInterestCompleted.toString()));
Money expectedPrincipalCompleted = new MoneyBuilder().with(usDollars).with(principal).build();
assertThat(installment.getPrincipalCompleted(usDollars).toString(), is(expectedPrincipalCompleted.toString()));
}
private void assertThatLoanTransactionHasDerivedFieldsOf(
final String principal,
final String interest,
final String interestWaived,
final LoanTransaction transaction, final MonetaryCurrency currency) {
Money expectedPrincipalPortion = new MoneyBuilder().with(currency).with(principal).build();
assertThat("Principal portion of transaction not as expected: ", transaction.getPrincipalPortion(currency).toString(), is(expectedPrincipalPortion.toString()));
Money expectedInterestPortion = new MoneyBuilder().with(currency).with(interest).build();
assertThat("Interest portion of transaction not as expected: ", transaction.getInterestPortion(currency).toString(), is(expectedInterestPortion.toString()));
Money expectedInterestWaivedPortion = new MoneyBuilder().with(currency).with(interestWaived).build();
assertThat("Interest waived portion of transaction not as expected: ",transaction.getInterestWaivedPortion(currency).toString(), is(expectedInterestWaivedPortion.toString()));
}
private void assertThatLoanInstallmentHasDerivedFieldsOf(
final String principal,
final String interest,
final String interestWaived,
final LoanRepaymentScheduleInstallment installment, final MonetaryCurrency currency) {
Money expectedPrincipalCompleted = new MoneyBuilder().with(currency).with(principal).build();
assertThat("principal not as expected", installment.getPrincipalCompleted(currency).toString(), is(expectedPrincipalCompleted.toString()));
Money expectedInterestCompleted = new MoneyBuilder().with(currency).with(interest).build();
assertThat("interest completed not as expected: ",installment.getInterestCompleted(currency).toString(), is(expectedInterestCompleted.toString()));
Money expectedInterestWaived = new MoneyBuilder().with(currency).with(interestWaived).build();
assertThat("interest waived not as expected: ", installment.getInterestWaived(currency).toString(), is(expectedInterestWaived.toString()));
}
private String principalOf(final String value) {
return value;
}
private String interestOf(final String value) {
return value;
}
private String interestWaivedOf(final String value) {
return value;
}
}
@@ -8,11 +8,11 @@ public class LoanRepaymentScheduleInstallmentBuilder {
private Loan loan = null; private Loan loan = null;
private Integer installmentNumber = Integer.valueOf(1); private Integer installmentNumber = Integer.valueOf(1);
// private LocalDate from = LocalDate.now();
private LocalDate dueDate = LocalDate.now(); private LocalDate dueDate = LocalDate.now();
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
private Money principal = new MoneyBuilder().build(); private Money principal = new MoneyBuilder().build();
private Money interest = new MoneyBuilder().build(); private Money interest = new MoneyBuilder().build();
private boolean completed = false;
public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) { public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail; this.currencyDetail = currencyDetail;
@@ -21,7 +21,12 @@ public class LoanRepaymentScheduleInstallmentBuilder {
} }
public LoanRepaymentScheduleInstallment build() { public LoanRepaymentScheduleInstallment build() {
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount()); LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount());
if (completed) {
installment.payPrincipalComponent(principal);
installment.payInterestComponent(interest);
}
return installment;
} }
public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) { public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) {
@@ -38,4 +43,9 @@ public class LoanRepaymentScheduleInstallmentBuilder {
this.dueDate = withDueDate; this.dueDate = withDueDate;
return this; return this;
} }
public LoanRepaymentScheduleInstallmentBuilder completed() {
this.completed = true;
return this;
}
} }
@@ -0,0 +1,60 @@
package org.mifosng.platform.loan.domain;
import java.util.Arrays;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
/**
* Helper class for creating loan schedule data suitable for testing.
*/
public class LoanScheduleTestDataHelper {
public static List<LoanRepaymentScheduleInstallment> createSimpleLoanSchedule(final LocalDate firstDueDate, final MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate)
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
LoanRepaymentScheduleInstallment secondInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate.plusMonths(1))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate.plusMonths(2))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
return Arrays.asList(firstInstallment, secondInstallment, thirdInstallment);
}
public static List<LoanRepaymentScheduleInstallment> createSimpleLoanScheduleWithFirstInstallmentFullyPaid(final LocalDate firstDueDate, final MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate)
.withPrincipal("1000.00")
.withInterest("200.00")
.completed()
.build();
LoanRepaymentScheduleInstallment secondInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate.plusMonths(1))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withDueDate(firstDueDate.plusMonths(2))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
return Arrays.asList(firstInstallment, secondInstallment, thirdInstallment);
}
}
@@ -5,17 +5,33 @@ import org.mifosng.platform.currency.domain.Money;
public class LoanTransactionBuilder { public class LoanTransactionBuilder {
private Money amount = new MoneyBuilder().build(); private Money transactionAmount = new MoneyBuilder().build();
private LocalDate paymentDate = LocalDate.now(); private LocalDate transactionDate = LocalDate.now();
private boolean repayment = false;
public LoanTransaction build() { public LoanTransaction build() {
return LoanTransaction.repayment(amount, paymentDate);
LoanTransaction transaction = null;
if (repayment) {
transaction = LoanTransaction.repayment(transactionAmount, transactionDate);
}
return transaction;
} }
public LoanTransactionBuilder with(Money newAmount) { public LoanTransactionBuilder with(final Money newAmount) {
this.amount = newAmount; this.transactionAmount = newAmount;
return this; return this;
} }
public LoanTransactionBuilder with(final LocalDate withTransactionDate) {
this.transactionDate = withTransactionDate;
return this;
}
public LoanTransactionBuilder repayment() {
this.repayment = true;
return this;
}
} }