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,
|
`principal_completed_derived` decimal(19,6) DEFAULT NULL,
|
||||||
`interest_amount` decimal(19,6) DEFAULT NULL,
|
`interest_amount` decimal(19,6) DEFAULT NULL,
|
||||||
`interest_completed_derived` decimal(19,6) DEFAULT NULL,
|
`interest_completed_derived` decimal(19,6) DEFAULT NULL,
|
||||||
|
`interest_waived_derived` decimal(19,6) DEFAULT NULL,
|
||||||
`completed_derived` bit(1) NOT NULL,
|
`completed_derived` bit(1) NOT NULL,
|
||||||
`createdby_id` bigint(20) DEFAULT NULL,
|
`createdby_id` bigint(20) DEFAULT NULL,
|
||||||
`created_date` datetime DEFAULT NULL,
|
`created_date` datetime DEFAULT NULL,
|
||||||
@@ -342,6 +343,8 @@ CREATE TABLE `portfolio_loan_transaction` (
|
|||||||
`contra_id` bigint(20) DEFAULT NULL,
|
`contra_id` bigint(20) DEFAULT NULL,
|
||||||
`transaction_date` date NOT NULL,
|
`transaction_date` date NOT NULL,
|
||||||
`amount` decimal(19,6) NOT NULL,
|
`amount` decimal(19,6) NOT NULL,
|
||||||
|
`principal_portion_derived` decimal(19,6) NOT NULL,
|
||||||
|
`interest_portion_derived` decimal(19,6) NOT NULL,
|
||||||
`createdby_id` bigint(20) DEFAULT NULL,
|
`createdby_id` bigint(20) DEFAULT NULL,
|
||||||
`created_date` datetime DEFAULT NULL,
|
`created_date` datetime DEFAULT NULL,
|
||||||
`lastmodified_date` datetime DEFAULT NULL,
|
`lastmodified_date` datetime DEFAULT NULL,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ VALUES
|
|||||||
(300, 'Active'),
|
(300, 'Active'),
|
||||||
(400, 'Withdrawn by client'),
|
(400, 'Withdrawn by client'),
|
||||||
(500, 'Rejected'),
|
(500, 'Rejected'),
|
||||||
(600, 'Closed');
|
(600, 'Closed'),
|
||||||
|
(700, 'Overpaid');
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO `ref_currency`
|
INSERT INTO `ref_currency`
|
||||||
|
|||||||
+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;
|
package org.mifosng.platform.api.data;
|
||||||
|
|
||||||
|
|
||||||
public class LoanAccountSummaryData {
|
public class LoanAccountSummaryData {
|
||||||
|
|
||||||
private MoneyData originalPrincipal;
|
private MoneyData originalPrincipal;
|
||||||
@@ -9,32 +8,42 @@ public class LoanAccountSummaryData {
|
|||||||
private MoneyData principalPaid;
|
private MoneyData principalPaid;
|
||||||
private MoneyData principalOutstanding;
|
private MoneyData principalOutstanding;
|
||||||
private MoneyData interestPaid;
|
private MoneyData interestPaid;
|
||||||
|
private MoneyData interestWaived;
|
||||||
private MoneyData interestOutstanding;
|
private MoneyData interestOutstanding;
|
||||||
private MoneyData totalPaid;
|
private MoneyData totalPaid;
|
||||||
|
private MoneyData totalWaived;
|
||||||
private MoneyData totalOutstanding;
|
private MoneyData totalOutstanding;
|
||||||
private MoneyData totalInArrears;
|
private MoneyData totalInArrears;
|
||||||
private MoneyData totalWaived;
|
|
||||||
|
|
||||||
protected LoanAccountSummaryData() {
|
protected LoanAccountSummaryData() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoanAccountSummaryData(MoneyData originalPrincipal,
|
public LoanAccountSummaryData(
|
||||||
MoneyData principalPaid, MoneyData principalOutstanding,
|
MoneyData originalPrincipal,
|
||||||
MoneyData originalInterest, MoneyData interestPaid,
|
MoneyData principalPaid,
|
||||||
MoneyData interestOutstanding, MoneyData originalTotal,
|
MoneyData principalOutstanding,
|
||||||
MoneyData totalPaid, MoneyData totalOutstanding, MoneyData totalInArrears, MoneyData totalWaived) {
|
MoneyData originalInterest,
|
||||||
|
MoneyData interestPaid,
|
||||||
|
MoneyData interestWaived,
|
||||||
|
MoneyData interestOutstanding,
|
||||||
|
MoneyData originalTotal,
|
||||||
|
MoneyData totalPaid,
|
||||||
|
MoneyData totalWaived,
|
||||||
|
MoneyData totalOutstanding,
|
||||||
|
MoneyData totalInArrears) {
|
||||||
this.originalPrincipal = originalPrincipal;
|
this.originalPrincipal = originalPrincipal;
|
||||||
this.principalPaid = principalPaid;
|
this.principalPaid = principalPaid;
|
||||||
this.principalOutstanding = principalOutstanding;
|
this.principalOutstanding = principalOutstanding;
|
||||||
this.originalInterest = originalInterest;
|
this.originalInterest = originalInterest;
|
||||||
this.interestPaid = interestPaid;
|
this.interestPaid = interestPaid;
|
||||||
|
this.interestWaived = interestWaived;
|
||||||
this.interestOutstanding = interestOutstanding;
|
this.interestOutstanding = interestOutstanding;
|
||||||
this.originalTotal = originalTotal;
|
this.originalTotal = originalTotal;
|
||||||
this.totalPaid = totalPaid;
|
this.totalPaid = totalPaid;
|
||||||
|
this.totalWaived = totalWaived;
|
||||||
this.totalOutstanding = totalOutstanding;
|
this.totalOutstanding = totalOutstanding;
|
||||||
this.totalInArrears = totalInArrears;
|
this.totalInArrears = totalInArrears;
|
||||||
this.totalWaived = totalWaived;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWaiveAllowed(MoneyData tolerance) {
|
public boolean isWaiveAllowed(MoneyData tolerance) {
|
||||||
@@ -89,6 +98,14 @@ public class LoanAccountSummaryData {
|
|||||||
this.interestPaid = interestPaid;
|
this.interestPaid = interestPaid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MoneyData getInterestWaived() {
|
||||||
|
return interestWaived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInterestWaived(MoneyData interestWaived) {
|
||||||
|
this.interestWaived = interestWaived;
|
||||||
|
}
|
||||||
|
|
||||||
public MoneyData getInterestOutstanding() {
|
public MoneyData getInterestOutstanding() {
|
||||||
return interestOutstanding;
|
return interestOutstanding;
|
||||||
}
|
}
|
||||||
@@ -105,6 +122,14 @@ public class LoanAccountSummaryData {
|
|||||||
this.totalPaid = totalPaid;
|
this.totalPaid = totalPaid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MoneyData getTotalWaived() {
|
||||||
|
return totalWaived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalWaived(MoneyData totalWaived) {
|
||||||
|
this.totalWaived = totalWaived;
|
||||||
|
}
|
||||||
|
|
||||||
public MoneyData getTotalOutstanding() {
|
public MoneyData getTotalOutstanding() {
|
||||||
return totalOutstanding;
|
return totalOutstanding;
|
||||||
}
|
}
|
||||||
@@ -120,12 +145,4 @@ public class LoanAccountSummaryData {
|
|||||||
public void setTotalInArrears(MoneyData totalInArrears) {
|
public void setTotalInArrears(MoneyData totalInArrears) {
|
||||||
this.totalInArrears = totalInArrears;
|
this.totalInArrears = totalInArrears;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneyData getTotalWaived() {
|
|
||||||
return totalWaived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalWaived(MoneyData totalWaived) {
|
|
||||||
this.totalWaived = totalWaived;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+22
-2
@@ -12,9 +12,11 @@ public class LoanRepaymentPeriodData {
|
|||||||
private MoneyData principalOutstanding;
|
private MoneyData principalOutstanding;
|
||||||
private MoneyData interest;
|
private MoneyData interest;
|
||||||
private MoneyData interestPaid;
|
private MoneyData interestPaid;
|
||||||
|
private MoneyData interestWaived;
|
||||||
private MoneyData interestOutstanding;
|
private MoneyData interestOutstanding;
|
||||||
private MoneyData total;
|
private MoneyData total;
|
||||||
private MoneyData totalPaid;
|
private MoneyData totalPaid;
|
||||||
|
private MoneyData totalWaived;
|
||||||
private MoneyData totalOutstanding;
|
private MoneyData totalOutstanding;
|
||||||
|
|
||||||
public LoanRepaymentPeriodData() {
|
public LoanRepaymentPeriodData() {
|
||||||
@@ -24,8 +26,8 @@ public class LoanRepaymentPeriodData {
|
|||||||
public LoanRepaymentPeriodData(Long loanId, Integer period,
|
public LoanRepaymentPeriodData(Long loanId, Integer period,
|
||||||
LocalDate date, MoneyData principal, MoneyData principalPaid,
|
LocalDate date, MoneyData principal, MoneyData principalPaid,
|
||||||
MoneyData principalOutstanding, MoneyData interest,
|
MoneyData principalOutstanding, MoneyData interest,
|
||||||
MoneyData interestPaid, MoneyData interestOutstanding,
|
MoneyData interestPaid, MoneyData interestWaived, MoneyData interestOutstanding,
|
||||||
MoneyData total, MoneyData totalPaid, MoneyData totalOutstanding) {
|
MoneyData total, MoneyData totalPaid, MoneyData totalWaived, MoneyData totalOutstanding) {
|
||||||
this.loanId = loanId;
|
this.loanId = loanId;
|
||||||
this.period = period;
|
this.period = period;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
@@ -34,9 +36,11 @@ public class LoanRepaymentPeriodData {
|
|||||||
this.principalOutstanding = principalOutstanding;
|
this.principalOutstanding = principalOutstanding;
|
||||||
this.interest = interest;
|
this.interest = interest;
|
||||||
this.interestPaid = interestPaid;
|
this.interestPaid = interestPaid;
|
||||||
|
this.interestWaived = interestWaived;
|
||||||
this.interestOutstanding = interestOutstanding;
|
this.interestOutstanding = interestOutstanding;
|
||||||
this.total = total;
|
this.total = total;
|
||||||
this.totalPaid = totalPaid;
|
this.totalPaid = totalPaid;
|
||||||
|
this.totalWaived = totalWaived;
|
||||||
this.totalOutstanding = totalOutstanding;
|
this.totalOutstanding = totalOutstanding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +108,14 @@ public class LoanRepaymentPeriodData {
|
|||||||
this.interestPaid = interestPaid;
|
this.interestPaid = interestPaid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MoneyData getInterestWaived() {
|
||||||
|
return interestWaived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInterestWaived(MoneyData interestWaived) {
|
||||||
|
this.interestWaived = interestWaived;
|
||||||
|
}
|
||||||
|
|
||||||
public MoneyData getInterestOutstanding() {
|
public MoneyData getInterestOutstanding() {
|
||||||
return interestOutstanding;
|
return interestOutstanding;
|
||||||
}
|
}
|
||||||
@@ -128,6 +140,14 @@ public class LoanRepaymentPeriodData {
|
|||||||
this.totalPaid = totalPaid;
|
this.totalPaid = totalPaid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MoneyData getTotalWaived() {
|
||||||
|
return totalWaived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalWaived(MoneyData totalWaived) {
|
||||||
|
this.totalWaived = totalWaived;
|
||||||
|
}
|
||||||
|
|
||||||
public MoneyData getTotalOutstanding() {
|
public MoneyData getTotalOutstanding() {
|
||||||
return totalOutstanding;
|
return totalOutstanding;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -54,7 +54,7 @@ public class DefaultLoanLifecycleStateMachine implements
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LOAN_REPAYMENT:
|
case LOAN_REPAYMENT:
|
||||||
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
|
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) {
|
||||||
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
|
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
|
||||||
} else {
|
} else {
|
||||||
newState = from;
|
newState = from;
|
||||||
@@ -80,6 +80,11 @@ public class DefaultLoanLifecycleStateMachine implements
|
|||||||
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
|
newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case LOAN_OVERPAYMENT:
|
||||||
|
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED, LoanStatus.ACTIVE)) {
|
||||||
|
newState = stateOf(LoanStatus.OVERPAID, allowedLoanStatuses);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
|
|||||||
+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.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -354,10 +355,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
this.disbursedOnDate = null;
|
this.disbursedOnDate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void waive(LoanTransaction loanTransaction,
|
public void waive(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||||
LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(
|
|
||||||
LoanEvent.LOAN_REPAYMENT, this.loanStatus);
|
|
||||||
loanTransaction.updateLoan(this);
|
loanTransaction.updateLoan(this);
|
||||||
this.loanTransactions.add(loanTransaction);
|
this.loanTransactions.add(loanTransaction);
|
||||||
|
|
||||||
@@ -395,49 +394,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
waived, getInArrearsTolerance());
|
waived, getInArrearsTolerance());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isRepaidInFull()) {
|
doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(
|
|
||||||
LoanEvent.REPAID_IN_FULL, this.loanStatus);
|
|
||||||
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
|
|
||||||
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
|
|
||||||
|
|
||||||
if (isInterestRebateAllowed()) {
|
|
||||||
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
|
|
||||||
.getTransactionDate());
|
|
||||||
if (rebateDue.isGreaterThanZero()) {
|
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(
|
|
||||||
LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
|
|
||||||
this.interestRebateOwed = rebateDue.getAmount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||||
|
handleRepaymentTransaction(loanTransaction, loanLifecycleStateMachine, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRepaymentTransaction(
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final LoanLifecycleStateMachine loanLifecycleStateMachine, final boolean adjusted) {
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
|
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus);
|
||||||
loanTransaction.updateLoan(this);
|
loanTransaction.updateLoan(this);
|
||||||
|
|
||||||
|
boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, this.loanTransactions);
|
||||||
|
|
||||||
this.loanTransactions.add(loanTransaction);
|
this.loanTransactions.add(loanTransaction);
|
||||||
|
|
||||||
deriveLoanRepaymentScheduleCompletedData();
|
|
||||||
|
|
||||||
// Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
|
|
||||||
// MonetaryCurrency currency = totalRepaidOrWaived.getCurrency();
|
|
||||||
//
|
|
||||||
// int repaymentIndex = 0;
|
|
||||||
//
|
|
||||||
// for (LoanTransaction transaction : this.loanTransactions) {
|
|
||||||
// if (transaction.isRepayment() || transaction.isWaiver()) {
|
|
||||||
// totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction.getAmount());
|
|
||||||
//
|
|
||||||
// LoanRepaymentScheduleInstallment currentInstallment = this.repaymentScheduleInstallments.get(repaymentIndex);
|
|
||||||
// if (currentInstallment.unpaid()) {
|
|
||||||
// currentInstallment.payOffWith(transaction, currency);
|
|
||||||
// } else {
|
|
||||||
// repaymentIndex++;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (loanTransaction.isNotRepayment()) {
|
if (loanTransaction.isNotRepayment()) {
|
||||||
final String errorMessage = "A transaction of type repayment was expected but not received.";
|
final String errorMessage = "A transaction of type repayment was expected but not received.";
|
||||||
throw new InvalidLoanTransactionTypeException("transaction",
|
throw new InvalidLoanTransactionTypeException("transaction",
|
||||||
@@ -459,65 +432,80 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
"cannot.be.a.futre.date", errorMessage, loanTransactionDate);
|
"cannot.be.a.futre.date", errorMessage, loanTransactionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isRepaidInFull()) {
|
List<LoanTransaction> repaymentsOrWaivers = new ArrayList<LoanTransaction>();
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(
|
for (LoanTransaction transaction : this.loanTransactions) {
|
||||||
LoanEvent.REPAID_IN_FULL, this.loanStatus);
|
if (!transaction.isDisbursement() && transaction.isNotContra()) {
|
||||||
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
|
repaymentsOrWaivers.add(transaction);
|
||||||
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
|
}
|
||||||
|
}
|
||||||
|
LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
|
||||||
|
Collections.sort(repaymentsOrWaivers, transactionComparator);
|
||||||
|
|
||||||
if (isInterestRebateAllowed()) {
|
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = new DefaultLoanRepaymentScheduleTransactionProcessor();
|
||||||
Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction
|
if (isTransactionChronologicallyLatest && !adjusted) {
|
||||||
.getTransactionDate());
|
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments);
|
||||||
if (rebateDue.isGreaterThanZero()) {
|
} else {
|
||||||
this.loanStatus = loanLifecycleStateMachine.transition(
|
loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments);
|
||||||
LoanEvent.INTERST_REBATE_OWED, this.loanStatus);
|
}
|
||||||
this.interestRebateOwed = rebateDue.getAmount();
|
|
||||||
|
doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doPostLoanTransactionChecks(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||||
|
|
||||||
|
if (this.isOverPaid()) {
|
||||||
|
handleLoanOverpayment(loanTransaction, loanLifecycleStateMachine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRepaidInFull()) {
|
||||||
|
handleLoanRepaymentInFull(loanTransaction, loanLifecycleStateMachine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
return isChronologicallyLatestRepaymentOrWaiver;
|
||||||
|
|
||||||
Money totalRepaidOrWaivedAgainstLoan = calculateTotalPaidOrWaived();
|
|
||||||
|
|
||||||
Money remainingToPayoffAgainstLoanSchedule = totalRepaidOrWaivedAgainstLoan;
|
|
||||||
Money totalOverpaid = Money.zero(totalRepaidOrWaivedAgainstLoan.getCurrency());
|
|
||||||
int repaymentInstallmentIndex = 0;
|
|
||||||
while (remainingToPayoffAgainstLoanSchedule.isGreaterThanZero()) {
|
|
||||||
|
|
||||||
if (repaymentInstallmentIndex == this.repaymentScheduleInstallments.size()) {
|
|
||||||
totalOverpaid = remainingToPayoffAgainstLoanSchedule;
|
|
||||||
|
|
||||||
// to exit while loop
|
|
||||||
remainingToPayoffAgainstLoanSchedule = remainingToPayoffAgainstLoanSchedule.minus(totalOverpaid);
|
|
||||||
} else {
|
|
||||||
LoanRepaymentScheduleInstallment scheduledRepaymentInstallment = this.repaymentScheduleInstallments.get(repaymentInstallmentIndex);
|
|
||||||
remainingToPayoffAgainstLoanSchedule = scheduledRepaymentInstallment.updateDerivedComponents(remainingToPayoffAgainstLoanSchedule);
|
|
||||||
repaymentInstallmentIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Money calculateTotalPaidOrWaived() {
|
|
||||||
|
|
||||||
Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail
|
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
|
|
||||||
for (LoanTransaction transaction : this.loanTransactions) {
|
|
||||||
if (transaction.isRepayment() || transaction.isWaiver()) {
|
|
||||||
totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction
|
|
||||||
.getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalRepaidOrWaived;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalDate possibleNextRepaymentDate() {
|
public LocalDate possibleNextRepaymentDate() {
|
||||||
LocalDate earliestUnpaidInstallmentDate = new LocalDate();
|
LocalDate earliestUnpaidInstallmentDate = new LocalDate();
|
||||||
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||||
if (installment.unpaid()) {
|
if (installment.isNotFullyCompleted()) {
|
||||||
earliestUnpaidInstallmentDate = installment.getDueDate();
|
earliestUnpaidInstallmentDate = installment.getDueDate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -545,7 +533,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
Money possibleNextRepaymentAmount = Money.zero(currency);
|
Money possibleNextRepaymentAmount = Money.zero(currency);
|
||||||
|
|
||||||
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||||
if (installment.unpaid()) {
|
if (installment.isNotFullyCompleted()) {
|
||||||
possibleNextRepaymentAmount = installment.getTotalDue(currency);
|
possibleNextRepaymentAmount = installment.getTotalDue(currency);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -568,7 +556,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
|
|
||||||
transactionForAdjustment.contra();
|
transactionForAdjustment.contra();
|
||||||
if (newTransactionDetail.isRepayment()) {
|
if (newTransactionDetail.isRepayment()) {
|
||||||
makeRepayment(newTransactionDetail, loanLifecycleStateMachine);
|
handleRepaymentTransaction(newTransactionDetail, loanLifecycleStateMachine, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTransactionDetail.isWaiver()) {
|
if (newTransactionDetail.isWaiver()) {
|
||||||
@@ -578,38 +566,41 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
|
|
||||||
private boolean isRepaidInFull() {
|
private boolean isRepaidInFull() {
|
||||||
|
|
||||||
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail
|
MonetaryCurrency currency = loanCurrency();
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail
|
Money cumulativeOriginalExpectedTotal = Money.zero(currency);
|
||||||
.getPrincipal().getCurrency());
|
Money cumulativeTotalPaid = Money.zero(currency);
|
||||||
Money cumulativeTotal = Money.zero(this.loanRepaymentScheduleDetail
|
Money cumulativeTotalWaived = Money.zero(currency);
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
|
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
Money cumulativeWaived = Money.zero(this.loanRepaymentScheduleDetail
|
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
|
|
||||||
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
|
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
|
||||||
cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment
|
cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency));
|
||||||
.getPrincipal(loanCurrency()));
|
cumulativeTotalPaid = cumulativeTotalPaid.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency)));
|
||||||
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
|
cumulativeTotalWaived = cumulativeTotalWaived.plus(scheduledRepayment.getInterestWaived(currency));
|
||||||
.getInterest(loanCurrency()));
|
|
||||||
cumulativeTotal = cumulativeTotal.plus(scheduledRepayment
|
|
||||||
.getTotal(loanCurrency()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (LoanTransaction transaction : this.loanTransactions) {
|
Money totalOutstanding = cumulativeOriginalExpectedTotal.minus(cumulativeTotalPaid.plus(cumulativeTotalWaived));
|
||||||
if (transaction.isRepayment()) {
|
boolean isRepaidInFull = totalOutstanding.isZero();
|
||||||
cumulativePaid = cumulativePaid.plus(transaction.getAmount());
|
|
||||||
}
|
return isRepaidInFull;
|
||||||
if (transaction.isWaiver()) {
|
}
|
||||||
cumulativeWaived = cumulativeWaived.plus(transaction
|
|
||||||
.getAmount());
|
private boolean isOverPaid() {
|
||||||
}
|
|
||||||
|
Money totalPaidInRepayments = getTotalPaidInRepayments();
|
||||||
|
|
||||||
|
MonetaryCurrency currency = loanCurrency();
|
||||||
|
|
||||||
|
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(
|
return totalPaidInRepayments.isGreaterThan(cumulativeTotalPaidOnInstallments);
|
||||||
cumulativeTotal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MonetaryCurrency loanCurrency() {
|
private MonetaryCurrency loanCurrency() {
|
||||||
@@ -914,7 +905,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
|
|
||||||
LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate);
|
LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate);
|
||||||
|
|
||||||
Money totalPaidToDate = this.getTotalPaid();
|
Money totalPaidToDate = this.getTotalPaidInRepayments();
|
||||||
|
|
||||||
Money totalOutstandingBasedOnExpectedMaturityDate = this
|
Money totalOutstandingBasedOnExpectedMaturityDate = this
|
||||||
.getTotalOutstanding();
|
.getTotalOutstanding();
|
||||||
@@ -937,13 +928,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Money getTotalOutstanding() {
|
public Money getTotalOutstanding() {
|
||||||
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(
|
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(getTotalPaidInRepayments());
|
||||||
getTotalPaid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money getTotalPaid() {
|
private Money getTotalPaidInRepayments() {
|
||||||
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail
|
Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
|
||||||
.getPrincipal().getCurrency());
|
|
||||||
|
|
||||||
for (LoanTransaction repayment : this.loanTransactions) {
|
for (LoanTransaction repayment : this.loanTransactions) {
|
||||||
if (repayment.isRepayment()) {
|
if (repayment.isRepayment()) {
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ public enum LoanEvent {
|
|||||||
|
|
||||||
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT,
|
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT,
|
||||||
//
|
//
|
||||||
REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED;
|
REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED, LOAN_OVERPAYMENT;
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-53
@@ -29,16 +29,19 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
|
|||||||
private final Integer installmentNumber;
|
private final Integer installmentNumber;
|
||||||
|
|
||||||
@Column(name = "principal_amount", scale=6, precision=19)
|
@Column(name = "principal_amount", scale=6, precision=19)
|
||||||
private BigDecimal principal;
|
private BigDecimal principal = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(name = "interest_amount", scale=6, precision=19)
|
@Column(name = "interest_amount", scale=6, precision=19)
|
||||||
private BigDecimal interest;
|
private BigDecimal interest = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(name = "principal_completed_derived", scale=6, precision=19)
|
@Column(name = "principal_completed_derived", scale=6, precision=19)
|
||||||
private BigDecimal principalCompleted;
|
private BigDecimal principalCompleted = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(name = "interest_completed_derived", scale = 6, precision = 19)
|
@Column(name = "interest_completed_derived", scale = 6, precision = 19)
|
||||||
private BigDecimal interestCompleted;
|
private BigDecimal interestCompleted = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
|
||||||
|
private BigDecimal interestWaived = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(name="completed_derived")
|
@Column(name="completed_derived")
|
||||||
private boolean completed;
|
private boolean completed;
|
||||||
@@ -82,40 +85,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
|
|||||||
return new LocalDate(this.dueDate);
|
return new LocalDate(this.dueDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Money updateDerivedComponents(Money totalAvailable) {
|
|
||||||
|
|
||||||
Money remaining = totalAvailable.copy();
|
|
||||||
|
|
||||||
boolean principalCompletedInFull = false;
|
|
||||||
boolean interestCompletedInFull = false;
|
|
||||||
|
|
||||||
Money interest = getInterest(totalAvailable.getCurrency());
|
|
||||||
Money principal = getPrincipal(totalAvailable.getCurrency());
|
|
||||||
|
|
||||||
// TODO - NEWFEATURE - configuration around order components of loan are paid off in.
|
|
||||||
// pay off interest
|
|
||||||
if (remaining.isGreaterThanOrEqualTo(interest)) {
|
|
||||||
this.interestCompleted = interest.getAmount();
|
|
||||||
interestCompletedInFull = true;
|
|
||||||
} else {
|
|
||||||
this.interestCompleted = remaining.getAmount();
|
|
||||||
}
|
|
||||||
remaining = remaining.minus(this.interestCompleted);
|
|
||||||
|
|
||||||
// pay off principal
|
|
||||||
if (remaining.isGreaterThanOrEqualTo(principal)) {
|
|
||||||
this.principalCompleted = principal.getAmount();
|
|
||||||
principalCompletedInFull = true;
|
|
||||||
} else {
|
|
||||||
this.principalCompleted = remaining.getAmount();
|
|
||||||
}
|
|
||||||
remaining = remaining.minus(this.principalCompleted);
|
|
||||||
|
|
||||||
this.completed = (principalCompletedInFull && interestCompletedInFull);
|
|
||||||
|
|
||||||
return remaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Money getPrincipal(MonetaryCurrency currency) {
|
public Money getPrincipal(MonetaryCurrency currency) {
|
||||||
return Money.of(currency, this.principal);
|
return Money.of(currency, this.principal);
|
||||||
}
|
}
|
||||||
@@ -132,6 +101,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
|
|||||||
return Money.of(currency, this.interestCompleted);
|
return Money.of(currency, this.interestCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Money getInterestWaived(MonetaryCurrency currency) {
|
||||||
|
return Money.of(currency, this.interestWaived);
|
||||||
|
}
|
||||||
|
|
||||||
public Money getTotal(MonetaryCurrency currency) {
|
public Money getTotal(MonetaryCurrency currency) {
|
||||||
return getPrincipal(currency).plus(getInterest(currency));
|
return getPrincipal(currency).plus(getInterest(currency));
|
||||||
}
|
}
|
||||||
@@ -140,29 +113,95 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
|
|||||||
this.loan = loan;
|
this.loan = loan;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean unpaid() {
|
public boolean isFullyCompleted() {
|
||||||
|
return this.completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNotFullyCompleted() {
|
||||||
return !this.completed;
|
return !this.completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
|
||||||
|
return !isPrincipalCompleted(currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPrincipalCompleted(final MonetaryCurrency currency) {
|
||||||
|
return getPrincipal(currency).minus(getPrincipalCompleted(currency)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
public Money getTotalDue(MonetaryCurrency currency) {
|
public Money getTotalDue(MonetaryCurrency currency) {
|
||||||
return getTotal(currency).minus(getTotalCompleted(currency));
|
final Money totalPaidOrWaived = getInterestWaived(currency).plus(getTotalCompleted(currency));
|
||||||
|
return getTotal(currency).minus(totalPaidOrWaived);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money getTotalCompleted(MonetaryCurrency currency) {
|
private Money getTotalCompleted(MonetaryCurrency currency) {
|
||||||
return getPrincipalCompleted(currency).plus(getInterestCompleted(currency));
|
return getPrincipalCompleted(currency).plus(getInterestCompleted(currency));
|
||||||
}
|
}
|
||||||
|
|
||||||
// public void payOffWith(final LoanTransaction transaction, final MonetaryCurrency currency) {
|
public void resetDerivedComponents() {
|
||||||
// Money transactionAmountRemaining = Money.of(currency, transaction.getAmount());
|
this.principalCompleted = BigDecimal.ZERO;
|
||||||
//
|
this.interestCompleted = BigDecimal.ZERO;
|
||||||
// Money interestDue = getInterest(currency);
|
this.interestWaived = BigDecimal.ZERO;
|
||||||
// if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
|
this.completed = false;
|
||||||
// this.interestCompleted = interestDue.getAmount();
|
}
|
||||||
// transactionAmountRemaining = transactionAmountRemaining.minus(interestDue);
|
|
||||||
// } else {
|
public Money payInterestComponent(final Money transactionAmountRemaining) {
|
||||||
// this.interestCompleted = transactionAmountRemaining.getAmount();
|
|
||||||
// }
|
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
|
||||||
//
|
|
||||||
// Money principalDue = getPrincipal(currency);
|
Money interestPortionOfTransaction = Money.zero(currency);
|
||||||
// }
|
|
||||||
|
Money interestExpected = getInterest(currency);
|
||||||
|
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
|
||||||
|
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
|
||||||
|
this.interestCompleted = getInterestCompleted(currency).plus(interestDue).getAmount();
|
||||||
|
interestPortionOfTransaction = interestPortionOfTransaction.plus(interestDue);
|
||||||
|
} else {
|
||||||
|
this.interestCompleted = getInterestCompleted(currency).plus(transactionAmountRemaining).getAmount();
|
||||||
|
interestPortionOfTransaction = interestPortionOfTransaction.plus(transactionAmountRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completed = getTotalDue(currency).isZero();
|
||||||
|
|
||||||
|
return interestPortionOfTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money payPrincipalComponent(final Money transactionAmountRemaining) {
|
||||||
|
|
||||||
|
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
|
||||||
|
Money principalPortionOfTransaction = Money.zero(currency);
|
||||||
|
|
||||||
|
Money principalExpected = getPrincipal(currency);
|
||||||
|
Money principalDue = principalExpected.minus(getPrincipalCompleted(currency));
|
||||||
|
if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) {
|
||||||
|
this.principalCompleted = getPrincipalCompleted(currency).plus(principalDue).getAmount();
|
||||||
|
principalPortionOfTransaction = principalPortionOfTransaction.plus(principalDue);
|
||||||
|
} else {
|
||||||
|
this.principalCompleted = getPrincipalCompleted(currency).plus(transactionAmountRemaining).getAmount();
|
||||||
|
principalPortionOfTransaction = principalPortionOfTransaction.plus(transactionAmountRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completed = getTotalDue(currency).isZero();
|
||||||
|
|
||||||
|
return principalPortionOfTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money waiveInterestComponent(final Money transactionAmountRemaining) {
|
||||||
|
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
|
||||||
|
Money waivedInterestPortionOfTransaction = Money.zero(currency);
|
||||||
|
|
||||||
|
Money interestExpected = getInterest(currency);
|
||||||
|
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
|
||||||
|
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
|
||||||
|
this.interestWaived = getInterestWaived(currency).plus(interestDue).getAmount();
|
||||||
|
waivedInterestPortionOfTransaction = waivedInterestPortionOfTransaction.plus(interestDue);
|
||||||
|
} else {
|
||||||
|
this.interestWaived = getInterestWaived(currency).plus(transactionAmountRemaining).getAmount();
|
||||||
|
waivedInterestPortionOfTransaction = waivedInterestPortionOfTransaction.plus(transactionAmountRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completed = getTotalDue(currency).isZero();
|
||||||
|
|
||||||
|
return waivedInterestPortionOfTransaction;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+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 WITHDRAWN_BY_CLIENT = 400;
|
||||||
public static final Integer REJECTED = 500;
|
public static final Integer REJECTED = 500;
|
||||||
public static final Integer CLOSED = 600;
|
public static final Integer CLOSED = 600;
|
||||||
|
public static final Integer OVERPAID = 700;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "id")
|
@Column(name = "id")
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
private final BigDecimal amount;
|
private final BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(name = "principal_portion_derived", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal principalPortion = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "interest_portion_derived", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal interestPortion = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
|
||||||
|
private BigDecimal interestWaivedPortion = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Temporal(TemporalType.DATE)
|
@Temporal(TemporalType.DATE)
|
||||||
@Column(name = "transaction_date", nullable=false)
|
@Column(name = "transaction_date", nullable=false)
|
||||||
private final Date dateOf;
|
private final Date dateOf;
|
||||||
@@ -155,4 +164,36 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
public boolean isNonZero() {
|
public boolean isNonZero() {
|
||||||
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
|
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This updates the derived fields of a loan transaction for the principal, interest and interest waived portions.
|
||||||
|
*
|
||||||
|
* This accumulates the values passed to the already existent values for each of the portions.
|
||||||
|
*/
|
||||||
|
public void updateComponents(final Money principal, final Money interest, final Money interestWaivedPortion) {
|
||||||
|
MonetaryCurrency currency = principal.getCurrency();
|
||||||
|
this.principalPortion = getPrincipalPortion(currency).plus(principal).getAmount();
|
||||||
|
this.interestPortion = getInterestPortion(currency).plus(interest).getAmount();
|
||||||
|
this.interestWaivedPortion = getInterestWaivedPortion(currency).plus(interestWaivedPortion).getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money getPrincipalPortion(MonetaryCurrency currency) {
|
||||||
|
return Money.of(currency, principalPortion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money getInterestPortion(MonetaryCurrency currency) {
|
||||||
|
return Money.of(currency, interestPortion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Money getInterestWaivedPortion(MonetaryCurrency currency) {
|
||||||
|
if (this.interestWaivedPortion == null) {
|
||||||
|
this.interestWaivedPortion = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
return Money.of(currency, interestWaivedPortion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetDerivedComponents() {
|
||||||
|
this.principalPortion = BigDecimal.ZERO;
|
||||||
|
this.interestPortion = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+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 principalOutstanding = BigDecimal.ZERO;
|
||||||
BigDecimal originalInterest = BigDecimal.ZERO;
|
BigDecimal originalInterest = BigDecimal.ZERO;
|
||||||
BigDecimal interestPaid = BigDecimal.ZERO;
|
BigDecimal interestPaid = BigDecimal.ZERO;
|
||||||
|
BigDecimal interestWaived = BigDecimal.ZERO;
|
||||||
BigDecimal interestOutstanding = BigDecimal.ZERO;
|
BigDecimal interestOutstanding = BigDecimal.ZERO;
|
||||||
BigDecimal originalTotal = BigDecimal.ZERO;
|
BigDecimal originalTotal = BigDecimal.ZERO;
|
||||||
BigDecimal totalPaid = BigDecimal.ZERO;
|
BigDecimal totalPaid = BigDecimal.ZERO;
|
||||||
BigDecimal totalOutstanding = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalInArrears = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalWaived = BigDecimal.ZERO;
|
BigDecimal totalWaived = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalOutstanding = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
BigDecimal totalInArrears = BigDecimal.ZERO;
|
||||||
|
|
||||||
for (LoanRepaymentPeriodData installment : repaymentSchedule) {
|
for (LoanRepaymentPeriodData installment : repaymentSchedule) {
|
||||||
originalPrincipal = originalPrincipal.add(installment
|
originalPrincipal = originalPrincipal.add(installment.getPrincipal().getAmount());
|
||||||
.getPrincipal().getAmount());
|
principalPaid = principalPaid.add(installment.getPrincipalPaid().getAmount());
|
||||||
principalPaid = principalPaid.add(installment.getPrincipalPaid()
|
principalOutstanding = principalOutstanding.add(installment.getPrincipalOutstanding().getAmount());
|
||||||
.getAmount());
|
|
||||||
principalOutstanding = principalOutstanding.add(installment
|
|
||||||
.getPrincipalOutstanding().getAmount());
|
|
||||||
|
|
||||||
originalInterest = originalInterest.add(installment.getInterest()
|
originalInterest = originalInterest.add(installment.getInterest().getAmount());
|
||||||
.getAmount());
|
interestPaid = interestPaid.add(installment.getInterestPaid().getAmount());
|
||||||
interestPaid = interestPaid.add(installment.getInterestPaid()
|
interestWaived = interestWaived.add(installment.getInterestWaived().getAmount());
|
||||||
.getAmount());
|
interestOutstanding = interestOutstanding.add(installment.getInterestOutstanding().getAmount());
|
||||||
interestOutstanding = interestOutstanding.add(installment
|
|
||||||
.getInterestOutstanding().getAmount());
|
|
||||||
|
|
||||||
originalTotal = originalTotal.add(installment.getTotal()
|
originalTotal = originalTotal.add(installment.getTotal().getAmount());
|
||||||
.getAmount());
|
|
||||||
totalPaid = totalPaid.add(installment.getTotalPaid().getAmount());
|
totalPaid = totalPaid.add(installment.getTotalPaid().getAmount());
|
||||||
totalOutstanding = totalOutstanding.add(installment
|
totalWaived = totalWaived.add(installment.getTotalWaived().getAmount());
|
||||||
.getTotalOutstanding().getAmount());
|
totalOutstanding = totalOutstanding.add(installment.getTotalOutstanding().getAmount());
|
||||||
|
|
||||||
if (installment.getDate().isBefore(new LocalDate())) {
|
if (installment.getDate().isBefore(new LocalDate())) {
|
||||||
totalInArrears = totalInArrears.add(installment
|
totalInArrears = totalInArrears.add(installment.getTotalOutstanding().getAmount());
|
||||||
.getTotalOutstanding().getAmount());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sum up all repayment transactions
|
||||||
Long waiverType = (long) 4;
|
Long waiverType = (long) 4;
|
||||||
|
MoneyData totalRepaymentAmount = MoneyData.of(currencyData, BigDecimal.ZERO);
|
||||||
for (LoanRepaymentTransactionData loanRepayment : loanRepayments) {
|
for (LoanRepaymentTransactionData loanRepayment : loanRepayments) {
|
||||||
Long transactionType = loanRepayment.getTransactionType().getId();
|
Long transactionType = loanRepayment.getTransactionType().getId();
|
||||||
if (transactionType.equals(waiverType)) {
|
if (transactionType.equals(waiverType)) {
|
||||||
totalWaived = totalWaived.add(loanRepayment.getTotal()
|
// skip
|
||||||
.getAmount());
|
} else {
|
||||||
|
totalRepaymentAmount = totalRepaymentAmount.plus(loanRepayment.getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LoanAccountSummaryData(MoneyData.of(currencyData,
|
// detect if loan is in overpaid state
|
||||||
originalPrincipal), MoneyData.of(currencyData, principalPaid),
|
if (totalRepaymentAmount.isGreaterThan(MoneyData.of(currencyData, totalPaid))) {
|
||||||
MoneyData.of(currencyData, principalOutstanding), MoneyData.of(
|
BigDecimal difference = totalRepaymentAmount.getAmount().subtract(totalPaid);
|
||||||
currencyData, originalInterest), MoneyData.of(
|
totalOutstanding = totalOutstanding.subtract(difference);
|
||||||
currencyData, interestPaid), MoneyData.of(currencyData,
|
}
|
||||||
interestOutstanding), MoneyData.of(currencyData,
|
|
||||||
originalTotal), MoneyData.of(currencyData, totalPaid),
|
return new LoanAccountSummaryData(
|
||||||
MoneyData.of(currencyData, totalOutstanding), MoneyData.of(
|
MoneyData.of(currencyData,originalPrincipal),
|
||||||
currencyData, totalInArrears), MoneyData.of(
|
MoneyData.of(currencyData, principalPaid),
|
||||||
currencyData, totalWaived));
|
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
|
@Override
|
||||||
@@ -524,7 +530,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
|||||||
|
|
||||||
return " ls.loan_id as loanId, ls.installment as period, ls.duedate as `date`, "
|
return " ls.loan_id as loanId, ls.installment as period, ls.duedate as `date`, "
|
||||||
+ " ls.principal_amount as principal, ls.principal_completed_derived as principalPaid, "
|
+ " ls.principal_amount as principal, ls.principal_completed_derived as principalPaid, "
|
||||||
+ " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, "
|
+ " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, "
|
||||||
+ " l.currency_code as currencyCode, l.currency_digits as currencyDigits, rc.`name` as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode "
|
+ " l.currency_code as currencyCode, l.currency_digits as currencyDigits, rc.`name` as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode "
|
||||||
+ " from portfolio_loan l "
|
+ " from portfolio_loan l "
|
||||||
+ " join portfolio_loan_repayment_schedule ls on ls.loan_id = l.id "
|
+ " join portfolio_loan_repayment_schedule ls on ls.loan_id = l.id "
|
||||||
@@ -553,29 +559,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
|||||||
BigDecimal principalBD = rs.getBigDecimal("principal");
|
BigDecimal principalBD = rs.getBigDecimal("principal");
|
||||||
MoneyData principal = MoneyData.of(currencyData, principalBD);
|
MoneyData principal = MoneyData.of(currencyData, principalBD);
|
||||||
BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid");
|
BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid");
|
||||||
MoneyData principalPaid = MoneyData.of(currencyData,
|
MoneyData principalPaid = MoneyData.of(currencyData, principalPaidBD);
|
||||||
principalPaidBD);
|
MoneyData principalOutstanding = MoneyData.of(currencyData,principalBD.subtract(principalPaidBD));
|
||||||
MoneyData principalOutstanding = MoneyData.of(currencyData,
|
|
||||||
principalBD.subtract(principalPaidBD));
|
|
||||||
|
|
||||||
BigDecimal interestBD = rs.getBigDecimal("interest");
|
BigDecimal interestBD = rs.getBigDecimal("interest");
|
||||||
MoneyData interest = MoneyData.of(currencyData, interestBD);
|
MoneyData interest = MoneyData.of(currencyData, interestBD);
|
||||||
BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid");
|
BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid");
|
||||||
MoneyData interestPaid = MoneyData.of(currencyData, interestPaidBD);
|
MoneyData interestPaid = MoneyData.of(currencyData, interestPaidBD);
|
||||||
MoneyData interestOutstanding = MoneyData.of(currencyData,
|
|
||||||
interestBD.subtract(interestPaidBD));
|
|
||||||
|
|
||||||
MoneyData total = MoneyData.of(currencyData,
|
BigDecimal interestWaivedBD = rs.getBigDecimal("interestWaived");
|
||||||
principalBD.add(interestBD));
|
if (interestWaivedBD == null) {
|
||||||
MoneyData totalPaid = MoneyData.of(currencyData,
|
interestWaivedBD = BigDecimal.ZERO;
|
||||||
principalPaidBD.add(interestPaidBD));
|
}
|
||||||
MoneyData totalOutstanding = MoneyData.of(currencyData,
|
MoneyData interestWaived = MoneyData.of(currencyData, interestWaivedBD);
|
||||||
principalBD.add(interestBD).subtract(principalPaidBD)
|
MoneyData interestOutstanding = MoneyData.of(currencyData, interestBD.subtract(interestPaidBD).subtract(interestWaivedBD));
|
||||||
.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,
|
return new LoanRepaymentPeriodData(loanId, period, date,
|
||||||
principal, principalPaid, principalOutstanding, interest,
|
principal, principalPaid, principalOutstanding, interest,
|
||||||
interestPaid, interestOutstanding, total, totalPaid,
|
interestPaid, interestWaived, interestOutstanding, total, totalPaid, totalWaived,
|
||||||
totalOutstanding);
|
totalOutstanding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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 Loan loan = null;
|
||||||
private Integer installmentNumber = Integer.valueOf(1);
|
private Integer installmentNumber = Integer.valueOf(1);
|
||||||
// private LocalDate from = LocalDate.now();
|
|
||||||
private LocalDate dueDate = LocalDate.now();
|
private LocalDate dueDate = LocalDate.now();
|
||||||
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
|
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
|
||||||
private Money principal = new MoneyBuilder().build();
|
private Money principal = new MoneyBuilder().build();
|
||||||
private Money interest = new MoneyBuilder().build();
|
private Money interest = new MoneyBuilder().build();
|
||||||
|
private boolean completed = false;
|
||||||
|
|
||||||
public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) {
|
public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) {
|
||||||
this.currencyDetail = currencyDetail;
|
this.currencyDetail = currencyDetail;
|
||||||
@@ -21,7 +21,12 @@ public class LoanRepaymentScheduleInstallmentBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LoanRepaymentScheduleInstallment build() {
|
public LoanRepaymentScheduleInstallment build() {
|
||||||
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount());
|
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount());
|
||||||
|
if (completed) {
|
||||||
|
installment.payPrincipalComponent(principal);
|
||||||
|
installment.payInterestComponent(interest);
|
||||||
|
}
|
||||||
|
return installment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) {
|
public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) {
|
||||||
@@ -38,4 +43,9 @@ public class LoanRepaymentScheduleInstallmentBuilder {
|
|||||||
this.dueDate = withDueDate;
|
this.dueDate = withDueDate;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanRepaymentScheduleInstallmentBuilder completed() {
|
||||||
|
this.completed = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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 {
|
public class LoanTransactionBuilder {
|
||||||
|
|
||||||
private Money amount = new MoneyBuilder().build();
|
private Money transactionAmount = new MoneyBuilder().build();
|
||||||
private LocalDate paymentDate = LocalDate.now();
|
private LocalDate transactionDate = LocalDate.now();
|
||||||
|
private boolean repayment = false;
|
||||||
|
|
||||||
public LoanTransaction build() {
|
public LoanTransaction build() {
|
||||||
return LoanTransaction.repayment(amount, paymentDate);
|
|
||||||
|
LoanTransaction transaction = null;
|
||||||
|
|
||||||
|
if (repayment) {
|
||||||
|
transaction = LoanTransaction.repayment(transactionAmount, transactionDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoanTransactionBuilder with(Money newAmount) {
|
public LoanTransactionBuilder with(final Money newAmount) {
|
||||||
this.amount = newAmount;
|
this.transactionAmount = newAmount;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanTransactionBuilder with(final LocalDate withTransactionDate) {
|
||||||
|
this.transactionDate = withTransactionDate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanTransactionBuilder repayment() {
|
||||||
|
this.repayment = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user