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,
`interest_amount` 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,
`createdby_id` bigint(20) DEFAULT NULL,
`created_date` datetime DEFAULT NULL,
@@ -342,6 +343,8 @@ CREATE TABLE `portfolio_loan_transaction` (
`contra_id` bigint(20) DEFAULT NULL,
`transaction_date` date 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,
`created_date` datetime DEFAULT NULL,
`lastmodified_date` datetime DEFAULT NULL,
@@ -8,7 +8,8 @@ VALUES
(300, 'Active'),
(400, 'Withdrawn by client'),
(500, 'Rejected'),
(600, 'Closed');
(600, 'Closed'),
(700, 'Overpaid');
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;
public class LoanAccountSummaryData {
private MoneyData originalPrincipal;
@@ -9,32 +8,42 @@ public class LoanAccountSummaryData {
private MoneyData principalPaid;
private MoneyData principalOutstanding;
private MoneyData interestPaid;
private MoneyData interestWaived;
private MoneyData interestOutstanding;
private MoneyData totalPaid;
private MoneyData totalWaived;
private MoneyData totalOutstanding;
private MoneyData totalInArrears;
private MoneyData totalWaived;
protected LoanAccountSummaryData() {
//
}
public LoanAccountSummaryData(MoneyData originalPrincipal,
MoneyData principalPaid, MoneyData principalOutstanding,
MoneyData originalInterest, MoneyData interestPaid,
MoneyData interestOutstanding, MoneyData originalTotal,
MoneyData totalPaid, MoneyData totalOutstanding, MoneyData totalInArrears, MoneyData totalWaived) {
public LoanAccountSummaryData(
MoneyData originalPrincipal,
MoneyData principalPaid,
MoneyData principalOutstanding,
MoneyData originalInterest,
MoneyData interestPaid,
MoneyData interestWaived,
MoneyData interestOutstanding,
MoneyData originalTotal,
MoneyData totalPaid,
MoneyData totalWaived,
MoneyData totalOutstanding,
MoneyData totalInArrears) {
this.originalPrincipal = originalPrincipal;
this.principalPaid = principalPaid;
this.principalOutstanding = principalOutstanding;
this.originalInterest = originalInterest;
this.interestPaid = interestPaid;
this.interestWaived = interestWaived;
this.interestOutstanding = interestOutstanding;
this.originalTotal = originalTotal;
this.totalPaid = totalPaid;
this.totalWaived = totalWaived;
this.totalOutstanding = totalOutstanding;
this.totalInArrears = totalInArrears;
this.totalWaived = totalWaived;
}
public boolean isWaiveAllowed(MoneyData tolerance) {
@@ -88,6 +97,14 @@ public class LoanAccountSummaryData {
public void setInterestPaid(MoneyData interestPaid) {
this.interestPaid = interestPaid;
}
public MoneyData getInterestWaived() {
return interestWaived;
}
public void setInterestWaived(MoneyData interestWaived) {
this.interestWaived = interestWaived;
}
public MoneyData getInterestOutstanding() {
return interestOutstanding;
@@ -104,6 +121,14 @@ public class LoanAccountSummaryData {
public void setTotalPaid(MoneyData totalPaid) {
this.totalPaid = totalPaid;
}
public MoneyData getTotalWaived() {
return totalWaived;
}
public void setTotalWaived(MoneyData totalWaived) {
this.totalWaived = totalWaived;
}
public MoneyData getTotalOutstanding() {
return totalOutstanding;
@@ -120,12 +145,4 @@ public class LoanAccountSummaryData {
public void setTotalInArrears(MoneyData 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 interest;
private MoneyData interestPaid;
private MoneyData interestWaived;
private MoneyData interestOutstanding;
private MoneyData total;
private MoneyData totalPaid;
private MoneyData totalWaived;
private MoneyData totalOutstanding;
public LoanRepaymentPeriodData() {
@@ -24,8 +26,8 @@ public class LoanRepaymentPeriodData {
public LoanRepaymentPeriodData(Long loanId, Integer period,
LocalDate date, MoneyData principal, MoneyData principalPaid,
MoneyData principalOutstanding, MoneyData interest,
MoneyData interestPaid, MoneyData interestOutstanding,
MoneyData total, MoneyData totalPaid, MoneyData totalOutstanding) {
MoneyData interestPaid, MoneyData interestWaived, MoneyData interestOutstanding,
MoneyData total, MoneyData totalPaid, MoneyData totalWaived, MoneyData totalOutstanding) {
this.loanId = loanId;
this.period = period;
this.date = date;
@@ -34,9 +36,11 @@ public class LoanRepaymentPeriodData {
this.principalOutstanding = principalOutstanding;
this.interest = interest;
this.interestPaid = interestPaid;
this.interestWaived = interestWaived;
this.interestOutstanding = interestOutstanding;
this.total = total;
this.totalPaid = totalPaid;
this.totalWaived = totalWaived;
this.totalOutstanding = totalOutstanding;
}
@@ -103,6 +107,14 @@ public class LoanRepaymentPeriodData {
public void setInterestPaid(MoneyData interestPaid) {
this.interestPaid = interestPaid;
}
public MoneyData getInterestWaived() {
return interestWaived;
}
public void setInterestWaived(MoneyData interestWaived) {
this.interestWaived = interestWaived;
}
public MoneyData getInterestOutstanding() {
return interestOutstanding;
@@ -127,6 +139,14 @@ public class LoanRepaymentPeriodData {
public void setTotalPaid(MoneyData totalPaid) {
this.totalPaid = totalPaid;
}
public MoneyData getTotalWaived() {
return totalWaived;
}
public void setTotalWaived(MoneyData totalWaived) {
this.totalWaived = totalWaived;
}
public MoneyData getTotalOutstanding() {
return totalOutstanding;
@@ -54,7 +54,7 @@ public class DefaultLoanLifecycleStateMachine implements
}
break;
case LOAN_REPAYMENT:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
} else {
newState = from;
@@ -80,6 +80,11 @@ public class DefaultLoanLifecycleStateMachine implements
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
}
break;
case LOAN_OVERPAYMENT:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.OVERPAID, allowedLoanStatuses);
}
break;
}
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.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -354,10 +355,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.disbursedOnDate = null;
}
public void waive(LoanTransaction loanTransaction,
LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.LOAN_REPAYMENT, this.loanStatus);
public void waive(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
loanTransaction.updateLoan(this);
this.loanTransactions.add(loanTransaction);
@@ -395,49 +394,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
waived, getInArrearsTolerance());
}
if (this.isRepaidInFull()) {
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();
}
}
}
doPostLoanTransactionChecks(loanTransaction, 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);
loanTransaction.updateLoan(this);
boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, this.loanTransactions);
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()) {
final String errorMessage = "A transaction of type repayment was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction",
@@ -458,66 +431,81 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
throw new InvalidLoanStateTransitionException("repayment",
"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()) {
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
handleLoanRepaymentInFull(loanTransaction, loanLifecycleStateMachine);
}
}
if (isInterestRebateAllowed()) {
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
.getTransactionDate());
if (rebateDue.isGreaterThanZero()) {
this.loanStatus = loanLifecycleStateMachine.transition(
LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
this.interestRebateOwed = rebateDue.getAmount();
private void handleLoanRepaymentInFull(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine 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();
}
}
}
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;
}
}
}
}
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;
return isChronologicallyLatestRepaymentOrWaiver;
}
public LocalDate possibleNextRepaymentDate() {
LocalDate earliestUnpaidInstallmentDate = new LocalDate();
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) {
if (installment.isNotFullyCompleted()) {
earliestUnpaidInstallmentDate = installment.getDueDate();
break;
}
@@ -545,7 +533,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
Money possibleNextRepaymentAmount = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.unpaid()) {
if (installment.isNotFullyCompleted()) {
possibleNextRepaymentAmount = installment.getTotalDue(currency);
break;
}
@@ -568,7 +556,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
transactionForAdjustment.contra();
if (newTransactionDetail.isRepayment()) {
makeRepayment(newTransactionDetail, loanLifecycleStateMachine);
handleRepaymentTransaction(newTransactionDetail, loanLifecycleStateMachine, true);
}
if (newTransactionDetail.isWaiver()) {
@@ -577,39 +565,42 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
private boolean isRepaidInFull() {
MonetaryCurrency currency = loanCurrency();
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail
.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());
Money cumulativeOriginalExpectedTotal = Money.zero(currency);
Money cumulativeTotalPaid = Money.zero(currency);
Money cumulativeTotalWaived = Money.zero(currency);
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment
.getPrincipal(loanCurrency()));
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
.getInterest(loanCurrency()));
cumulativeTotal = cumulativeTotal.plus(scheduledRepayment
.getTotal(loanCurrency()));
cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency));
cumulativeTotalPaid = cumulativeTotalPaid.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency)));
cumulativeTotalWaived = cumulativeTotalWaived.plus(scheduledRepayment.getInterestWaived(currency));
}
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) {
if (transaction.isRepayment()) {
cumulativePaid = cumulativePaid.plus(transaction.getAmount());
}
if (transaction.isWaiver()) {
cumulativeWaived = cumulativeWaived.plus(transaction
.getAmount());
}
Money cumulativeOriginalExpectedTotal = Money.zero(currency);
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency));
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency)));
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
}
return cumulativePaid.plus(cumulativeWaived).isGreaterThanOrEqualTo(
cumulativeTotal);
return totalPaidInRepayments.isGreaterThan(cumulativeTotalPaidOnInstallments);
}
private MonetaryCurrency loanCurrency() {
@@ -914,7 +905,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate);
Money totalPaidToDate = this.getTotalPaid();
Money totalPaidToDate = this.getTotalPaidInRepayments();
Money totalOutstandingBasedOnExpectedMaturityDate = this
.getTotalOutstanding();
@@ -937,13 +928,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
public Money getTotalOutstanding() {
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(
getTotalPaid());
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(getTotalPaidInRepayments());
}
private Money getTotalPaid() {
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
for (LoanTransaction repayment : this.loanTransactions) {
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,
//
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;
@Column(name = "principal_amount", scale=6, precision=19)
private BigDecimal principal;
private BigDecimal principal = BigDecimal.ZERO;
@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)
private BigDecimal principalCompleted;
private BigDecimal principalCompleted = BigDecimal.ZERO;
@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")
private boolean completed;
@@ -82,40 +85,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
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) {
return Money.of(currency, this.principal);
}
@@ -132,6 +101,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return Money.of(currency, this.interestCompleted);
}
public Money getInterestWaived(MonetaryCurrency currency) {
return Money.of(currency, this.interestWaived);
}
public Money getTotal(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterest(currency));
}
@@ -139,30 +112,96 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
public void updateLoan(final Loan loan) {
this.loan = loan;
}
public boolean isFullyCompleted() {
return this.completed;
}
public boolean unpaid() {
public boolean isNotFullyCompleted() {
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) {
return getTotal(currency).minus(getTotalCompleted(currency));
final Money totalPaidOrWaived = getInterestWaived(currency).plus(getTotalCompleted(currency));
return getTotal(currency).minus(totalPaidOrWaived);
}
private Money getTotalCompleted(MonetaryCurrency currency) {
return getPrincipalCompleted(currency).plus(getInterestCompleted(currency));
}
// public void payOffWith(final LoanTransaction transaction, final MonetaryCurrency currency) {
// Money transactionAmountRemaining = Money.of(currency, transaction.getAmount());
//
// Money interestDue = getInterest(currency);
// if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
// this.interestCompleted = interestDue.getAmount();
// transactionAmountRemaining = transactionAmountRemaining.minus(interestDue);
// } else {
// this.interestCompleted = transactionAmountRemaining.getAmount();
// }
//
// Money principalDue = getPrincipal(currency);
// }
public void resetDerivedComponents() {
this.principalCompleted = BigDecimal.ZERO;
this.interestCompleted = BigDecimal.ZERO;
this.interestWaived = BigDecimal.ZERO;
this.completed = false;
}
public Money payInterestComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
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 REJECTED = 500;
public static final Integer CLOSED = 600;
public static final Integer OVERPAID = 700;
@Id
@Column(name = "id")
@@ -37,6 +37,15 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
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)
@Column(name = "transaction_date", nullable=false)
private final Date dateOf;
@@ -155,4 +164,36 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
public boolean isNonZero() {
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 originalInterest = BigDecimal.ZERO;
BigDecimal interestPaid = BigDecimal.ZERO;
BigDecimal interestWaived = BigDecimal.ZERO;
BigDecimal interestOutstanding = BigDecimal.ZERO;
BigDecimal originalTotal = BigDecimal.ZERO;
BigDecimal totalPaid = BigDecimal.ZERO;
BigDecimal totalOutstanding = BigDecimal.ZERO;
BigDecimal totalInArrears = BigDecimal.ZERO;
BigDecimal totalWaived = BigDecimal.ZERO;
BigDecimal totalOutstanding = BigDecimal.ZERO;
BigDecimal totalInArrears = BigDecimal.ZERO;
for (LoanRepaymentPeriodData installment : repaymentSchedule) {
originalPrincipal = originalPrincipal.add(installment
.getPrincipal().getAmount());
principalPaid = principalPaid.add(installment.getPrincipalPaid()
.getAmount());
principalOutstanding = principalOutstanding.add(installment
.getPrincipalOutstanding().getAmount());
originalPrincipal = originalPrincipal.add(installment.getPrincipal().getAmount());
principalPaid = principalPaid.add(installment.getPrincipalPaid().getAmount());
principalOutstanding = principalOutstanding.add(installment.getPrincipalOutstanding().getAmount());
originalInterest = originalInterest.add(installment.getInterest()
.getAmount());
interestPaid = interestPaid.add(installment.getInterestPaid()
.getAmount());
interestOutstanding = interestOutstanding.add(installment
.getInterestOutstanding().getAmount());
originalInterest = originalInterest.add(installment.getInterest().getAmount());
interestPaid = interestPaid.add(installment.getInterestPaid().getAmount());
interestWaived = interestWaived.add(installment.getInterestWaived().getAmount());
interestOutstanding = interestOutstanding.add(installment.getInterestOutstanding().getAmount());
originalTotal = originalTotal.add(installment.getTotal()
.getAmount());
originalTotal = originalTotal.add(installment.getTotal().getAmount());
totalPaid = totalPaid.add(installment.getTotalPaid().getAmount());
totalOutstanding = totalOutstanding.add(installment
.getTotalOutstanding().getAmount());
totalWaived = totalWaived.add(installment.getTotalWaived().getAmount());
totalOutstanding = totalOutstanding.add(installment.getTotalOutstanding().getAmount());
if (installment.getDate().isBefore(new LocalDate())) {
totalInArrears = totalInArrears.add(installment
.getTotalOutstanding().getAmount());
totalInArrears = totalInArrears.add(installment.getTotalOutstanding().getAmount());
}
}
// sum up all repayment transactions
Long waiverType = (long) 4;
MoneyData totalRepaymentAmount = MoneyData.of(currencyData, BigDecimal.ZERO);
for (LoanRepaymentTransactionData loanRepayment : loanRepayments) {
Long transactionType = loanRepayment.getTransactionType().getId();
if (transactionType.equals(waiverType)) {
totalWaived = totalWaived.add(loanRepayment.getTotal()
.getAmount());
// skip
} 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,
originalPrincipal), MoneyData.of(currencyData, principalPaid),
MoneyData.of(currencyData, principalOutstanding), MoneyData.of(
currencyData, originalInterest), MoneyData.of(
currencyData, interestPaid), MoneyData.of(currencyData,
interestOutstanding), MoneyData.of(currencyData,
originalTotal), MoneyData.of(currencyData, totalPaid),
MoneyData.of(currencyData, totalOutstanding), MoneyData.of(
currencyData, totalInArrears), MoneyData.of(
currencyData, totalWaived));
return new LoanAccountSummaryData(
MoneyData.of(currencyData,originalPrincipal),
MoneyData.of(currencyData, principalPaid),
MoneyData.of(currencyData, principalOutstanding),
MoneyData.of(currencyData, originalInterest),
MoneyData.of(currencyData, interestPaid),
MoneyData.of(currencyData, interestWaived),
MoneyData.of(currencyData, interestOutstanding),
MoneyData.of(currencyData, originalTotal),
MoneyData.of(currencyData, totalPaid),
MoneyData.of(currencyData, totalWaived),
MoneyData.of(currencyData, totalOutstanding),
MoneyData.of(currencyData, totalInArrears));
}
@Override
@@ -524,7 +530,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
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.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 "
+ " from portfolio_loan l "
+ " 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");
MoneyData principal = MoneyData.of(currencyData, principalBD);
BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid");
MoneyData principalPaid = MoneyData.of(currencyData,
principalPaidBD);
MoneyData principalOutstanding = MoneyData.of(currencyData,
principalBD.subtract(principalPaidBD));
MoneyData principalPaid = MoneyData.of(currencyData, principalPaidBD);
MoneyData principalOutstanding = MoneyData.of(currencyData,principalBD.subtract(principalPaidBD));
BigDecimal interestBD = rs.getBigDecimal("interest");
MoneyData interest = MoneyData.of(currencyData, interestBD);
BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid");
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,
principalBD.add(interestBD));
MoneyData totalPaid = MoneyData.of(currencyData,
principalPaidBD.add(interestPaidBD));
MoneyData totalOutstanding = MoneyData.of(currencyData,
principalBD.add(interestBD).subtract(principalPaidBD)
.subtract(interestPaidBD));
MoneyData total = MoneyData.of(currencyData,principalBD.add(interestBD));
MoneyData totalPaid = principalPaid.plus(interestPaid);
MoneyData totalWaived = interestWaived;
MoneyData totalOutstanding = principalOutstanding.plus(interestOutstanding);
return new LoanRepaymentPeriodData(loanId, period, date,
principal, principalPaid, principalOutstanding, interest,
interestPaid, interestOutstanding, total, totalPaid,
interestPaid, interestWaived, interestOutstanding, total, totalPaid, totalWaived,
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 Integer installmentNumber = Integer.valueOf(1);
// private LocalDate from = LocalDate.now();
private LocalDate dueDate = LocalDate.now();
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
private Money principal = new MoneyBuilder().build();
private Money interest = new MoneyBuilder().build();
private boolean completed = false;
public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail;
@@ -21,7 +21,12 @@ public class LoanRepaymentScheduleInstallmentBuilder {
}
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) {
@@ -38,4 +43,9 @@ public class LoanRepaymentScheduleInstallmentBuilder {
this.dueDate = withDueDate;
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 {
private Money amount = new MoneyBuilder().build();
private LocalDate paymentDate = LocalDate.now();
private Money transactionAmount = new MoneyBuilder().build();
private LocalDate transactionDate = LocalDate.now();
private boolean repayment = false;
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) {
this.amount = newAmount;
public LoanTransactionBuilder with(final Money newAmount) {
this.transactionAmount = newAmount;
return this;
}
public LoanTransactionBuilder with(final LocalDate withTransactionDate) {
this.transactionDate = withTransactionDate;
return this;
}
public LoanTransactionBuilder repayment() {
this.repayment = true;
return this;
}
}