support flexible repayment strategies for loans.
This commit is contained in:
@@ -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`
|
||||
|
||||
+12
@@ -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');
|
||||
+33
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -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;
|
||||
|
||||
+6
-1
@@ -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;
|
||||
|
||||
+204
@@ -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;
|
||||
}
|
||||
|
||||
+92
-53
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
-48
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+392
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user