create two types of repayment strategy from abstract template.
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} which is more convenient for concrete implementations to extend.
|
||||||
|
*
|
||||||
|
* @see HeavensFamilyLoanRepaymentScheduleTransactionProcessor
|
||||||
|
* @see CreocoreLoanRepaymentScheduleTransactionProcessor
|
||||||
|
*/
|
||||||
|
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides support for passing all {@link LoanTransaction}'s so it will
|
||||||
|
* completely re-process the entire loan schedule. This is required in cases
|
||||||
|
* where the {@link LoanTransaction} being processed is in the past and
|
||||||
|
* falls before existing transactions or and adjustment is made to an
|
||||||
|
* existing in which case the entire loan schedule needs to be re-processed.
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides support for processing the latest transaction (which should be latest transaction) against the loan schedule.
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
int installmentIndex = 0;
|
||||||
|
|
||||||
|
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
|
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||||
|
|
||||||
|
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||||
|
|
||||||
|
if (currentInstallment.isNotFullyCompleted()) {
|
||||||
|
|
||||||
|
// is this transaction early/late/on-time with respect to the current installment?
|
||||||
|
if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate, transactionAmountUnprocessed)) {
|
||||||
|
transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment, installments, loanTransaction, transactionDate, transactionAmountUnprocessed);
|
||||||
|
} else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments, loanTransaction.getTransactionDate())) {
|
||||||
|
// does this result in a late payment of existing installment?
|
||||||
|
transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments, loanTransaction, transactionAmountUnprocessed);
|
||||||
|
} else {
|
||||||
|
// standard transaction
|
||||||
|
transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installmentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||||
|
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// abstract interface
|
||||||
|
/**
|
||||||
|
* This method is responsible for checking if the current transaction is 'an
|
||||||
|
* advance/early payment' based on the details passed through.
|
||||||
|
*
|
||||||
|
* Default implementation simply processes transactions as 'Late' if the
|
||||||
|
* transaction date is after the installment due date.
|
||||||
|
*/
|
||||||
|
protected boolean isTransactionALateRepaymentOnInstallment(final int installmentIndex,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LocalDate transactionDate) {
|
||||||
|
|
||||||
|
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installmentIndex);
|
||||||
|
|
||||||
|
return transactionDate.isAfter(currentInstallment.getDueDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For late repayments, how should components of installment be paid off
|
||||||
|
*/
|
||||||
|
protected abstract Money handleTransactionThatIsALateRepaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is responsible for checking if the current transaction is 'an
|
||||||
|
* advance/early payment' based on the details passed through.
|
||||||
|
*/
|
||||||
|
protected abstract boolean isTransactionInAdvanceOfInstallment(
|
||||||
|
final int currentInstallmentIndex,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LocalDate transactionDate,
|
||||||
|
final Money transactionAmount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For early/'in advance' repayments.
|
||||||
|
*/
|
||||||
|
protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final LocalDate transactionDate,
|
||||||
|
final Money paymentInAdvance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For normal on-time repayments.
|
||||||
|
*/
|
||||||
|
protected abstract Money handleTransactionThatIsOnTimePaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when a transaction results in an over-payment of the full loan.
|
||||||
|
*
|
||||||
|
* transaction amount is greater than the total expected principal and interest of the loan.
|
||||||
|
*/
|
||||||
|
protected abstract void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount);
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package org.mifosng.platform.loan.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.mifosng.platform.currency.domain.Money;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creocore style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||||
|
*
|
||||||
|
* For standard transactions, pays off components in order of interest, then
|
||||||
|
* principal.
|
||||||
|
*
|
||||||
|
* If a transaction results in an advance payment or over-payment for a given
|
||||||
|
* installment, the over paid amount is pay off on the principal component of
|
||||||
|
* subsequent installments.
|
||||||
|
*
|
||||||
|
* If the entire principal of an installment is paid in advance then the
|
||||||
|
* interest component is waived.
|
||||||
|
*/
|
||||||
|
public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
|
||||||
|
AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For creocore, early is defined as any date before the installment due
|
||||||
|
* date
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean isTransactionInAdvanceOfInstallment(
|
||||||
|
final int currentInstallmentIndex,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LocalDate transactionDate, final Money transactionAmount) {
|
||||||
|
|
||||||
|
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
|
||||||
|
|
||||||
|
return transactionDate.isBefore(currentInstallment.getDueDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For early/'in advance' repayments, pays off principal component only.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final LocalDate transactionDate, final Money paymentInAdvance) {
|
||||||
|
|
||||||
|
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For late repayments, pay off in the same way as on-time payments,
|
||||||
|
* interest first then principal.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed) {
|
||||||
|
|
||||||
|
return handleTransactionThatIsOnTimePaymentOfInstallment(
|
||||||
|
currentInstallment, loanTransaction,
|
||||||
|
transactionAmountUnprocessed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For normal on-time repayments, pays off interest first, then principal.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed) {
|
||||||
|
|
||||||
|
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||||
|
|
||||||
|
Money interestPortion = currentInstallment
|
||||||
|
.payInterestComponent(transactionAmountRemaining);
|
||||||
|
transactionAmountRemaining = transactionAmountRemaining
|
||||||
|
.minus(interestPortion);
|
||||||
|
|
||||||
|
Money principalPortion = currentInstallment
|
||||||
|
.payPrincipalComponent(transactionAmountRemaining);
|
||||||
|
transactionAmountRemaining = transactionAmountRemaining
|
||||||
|
.minus(principalPortion);
|
||||||
|
|
||||||
|
Money interestWaivedPortion = Money.zero(transactionAmountRemaining
|
||||||
|
.getCurrency());
|
||||||
|
loanTransaction.updateComponents(principalPortion, interestPortion,
|
||||||
|
interestWaivedPortion);
|
||||||
|
|
||||||
|
return transactionAmountRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLoanOverpayment(final LoanTransaction loanTransaction,
|
||||||
|
final Money loanOverPaymentAmount) {
|
||||||
|
// dont do anything for with loan over-payment
|
||||||
|
}
|
||||||
|
}
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
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(currentInstallment, 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
|
|
||||||
* @param currentInstallment2
|
|
||||||
*/
|
|
||||||
protected void handleLateTransaction(
|
|
||||||
final LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
final List<LoanRepaymentScheduleInstallment> installments,
|
|
||||||
final LoanTransaction loanTransaction,
|
|
||||||
final MonetaryCurrency currency) {
|
|
||||||
|
|
||||||
Money installmentOverPaymentAmount = handleOnTimeTransaction(currentInstallment, loanTransaction, currency);
|
|
||||||
Money loanOverPaymentAmount = handleOverpaymentTransaction(installments, loanTransaction, currency, installmentOverPaymentAmount);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
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 advance payment or overpayment for a given
|
||||||
|
* installment, the over paid amount is pay off on the principal component of
|
||||||
|
* subsequent installments.
|
||||||
|
*
|
||||||
|
* If the entire principal of an installment is paid in advance then the interest component is waived.
|
||||||
|
*/
|
||||||
|
public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For late repayments, pay off in the same way as on-time payments,
|
||||||
|
* interest first then principal.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed) {
|
||||||
|
|
||||||
|
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isTransactionInAdvanceOfInstallment(
|
||||||
|
final int currentInstallmentIndex,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LocalDate transactionDate,
|
||||||
|
final Money transactionAmount) {
|
||||||
|
|
||||||
|
boolean isInAdvance = false;
|
||||||
|
|
||||||
|
LocalDate lastInstallmentDueDate = null;
|
||||||
|
if (currentInstallmentIndex > 0) {
|
||||||
|
int previousInstallmentIndex = currentInstallmentIndex - 1;
|
||||||
|
LoanRepaymentScheduleInstallment previousInstallment = installments.get(previousInstallmentIndex);
|
||||||
|
lastInstallmentDueDate = previousInstallment.getDueDate();
|
||||||
|
|
||||||
|
return !transactionDate.isAfter(lastInstallmentDueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInAdvance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For early/'in advance' repayments, pays off principal component only.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final LocalDate transactionDate, final Money paymentInAdvance) {
|
||||||
|
|
||||||
|
Money transactionAmountRemaining = paymentInAdvance;
|
||||||
|
MonetaryCurrency currency = paymentInAdvance.getCurrency();
|
||||||
|
|
||||||
|
// placeholders for tracking the breakdown of components relevant to
|
||||||
|
// this transaction.
|
||||||
|
Money principalPortion = Money.zero(currency);
|
||||||
|
Money interestPortion = Money.zero(currency);
|
||||||
|
Money interestWaivedPortion = Money.zero(currency);
|
||||||
|
|
||||||
|
if (currentInstallment.isPrincipalNotCompleted(currency)) {
|
||||||
|
principalPortion = currentInstallment
|
||||||
|
.payPrincipalComponent(transactionAmountRemaining);
|
||||||
|
if (currentInstallment.isPrincipalCompleted(currency)) {
|
||||||
|
interestWaivedPortion = currentInstallment
|
||||||
|
.waiveInterestComponent(currentInstallment
|
||||||
|
.getInterest(currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
loanTransaction.updateComponents(principalPortion, interestPortion,
|
||||||
|
interestWaivedPortion);
|
||||||
|
|
||||||
|
transactionAmountRemaining = transactionAmountRemaining
|
||||||
|
.minus(principalPortion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionAmountRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For normal on-time repayments, pays off interest first, then principal.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(
|
||||||
|
final LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money transactionAmountUnprocessed) {
|
||||||
|
|
||||||
|
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||||
|
|
||||||
|
Money interestPortion = currentInstallment
|
||||||
|
.payInterestComponent(transactionAmountRemaining);
|
||||||
|
transactionAmountRemaining = transactionAmountRemaining
|
||||||
|
.minus(interestPortion);
|
||||||
|
|
||||||
|
Money principalPortion = currentInstallment
|
||||||
|
.payPrincipalComponent(transactionAmountRemaining);
|
||||||
|
transactionAmountRemaining = transactionAmountRemaining
|
||||||
|
.minus(principalPortion);
|
||||||
|
|
||||||
|
Money interestWaivedPortion = Money.zero(transactionAmountRemaining
|
||||||
|
.getCurrency());
|
||||||
|
loanTransaction.updateComponents(principalPortion, interestPortion,
|
||||||
|
interestWaivedPortion);
|
||||||
|
|
||||||
|
return transactionAmountRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLoanOverpayment(
|
||||||
|
final LoanTransaction loanTransaction,
|
||||||
|
final Money loanOverPaymentAmount) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,6 +132,9 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory();
|
private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory();
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessor = new LoanRepaymentScheduleTransactionProcessorFactory();
|
||||||
|
|
||||||
public static Loan createNew(final Fund fund,
|
public static Loan createNew(final Fund fund,
|
||||||
final LoanProduct loanProduct, final Client client,
|
final LoanProduct loanProduct, final Client client,
|
||||||
@@ -441,7 +444,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
|
|||||||
LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
|
LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
|
||||||
Collections.sort(repaymentsOrWaivers, transactionComparator);
|
Collections.sort(repaymentsOrWaivers, transactionComparator);
|
||||||
|
|
||||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = new DefaultLoanRepaymentScheduleTransactionProcessor();
|
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor();
|
||||||
if (isTransactionChronologicallyLatest && !adjusted) {
|
if (isTransactionChronologicallyLatest && !adjusted) {
|
||||||
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments);
|
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package org.mifosng.platform.loan.domain;
|
||||||
|
|
||||||
|
import org.mifosng.platform.infrastructure.MifosPlatformTenant;
|
||||||
|
import org.mifosng.platform.infrastructure.ThreadLocalContextUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class LoanRepaymentScheduleTransactionProcessorFactory {
|
||||||
|
|
||||||
|
public LoanRepaymentScheduleTransactionProcessor determineProcessor() {
|
||||||
|
|
||||||
|
LoanRepaymentScheduleTransactionProcessor processor = new HeavensFamilyLoanRepaymentScheduleTransactionProcessor();
|
||||||
|
|
||||||
|
// FIXME - Just temp way of deciding what 'repayment strategy' to use for now.
|
||||||
|
MifosPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
|
||||||
|
if ("mifostenant-creocore".equalsIgnoreCase(tenant.getSchemaName())) {
|
||||||
|
processor = new CreocoreLoanRepaymentScheduleTransactionProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
return processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
-3
@@ -14,10 +14,10 @@ import org.mifosng.platform.currency.domain.Money;
|
|||||||
import org.mockito.runners.MockitoJUnitRunner;
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
public class DefaultLoanRepaymentScheduleTransactionProcessorTest {
|
public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest {
|
||||||
|
|
||||||
// class under test
|
// class under test
|
||||||
private DefaultLoanRepaymentScheduleTransactionProcessor processor;
|
private HeavensFamilyLoanRepaymentScheduleTransactionProcessor processor;
|
||||||
|
|
||||||
//
|
//
|
||||||
private LocalDate july2nd = new LocalDate(2012, 7, 2);
|
private LocalDate july2nd = new LocalDate(2012, 7, 2);
|
||||||
@@ -29,7 +29,7 @@ public class DefaultLoanRepaymentScheduleTransactionProcessorTest {
|
|||||||
|
|
||||||
installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars);
|
installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars);
|
||||||
|
|
||||||
processor = new DefaultLoanRepaymentScheduleTransactionProcessor();
|
processor = new HeavensFamilyLoanRepaymentScheduleTransactionProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,6 +326,36 @@ public class DefaultLoanRepaymentScheduleTransactionProcessorTest {
|
|||||||
|
|
||||||
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", paymentBeforeDueDateButNotInAdvance);
|
assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", paymentBeforeDueDateButNotInAdvance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario 12: Single transaction which is a repayment many days early but still does not qualify for 'in advance'.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void givenLateRepaymentOfInstallmentShouldPayoffInterestAndPrincipalComponentsOfOverdueInstallmentsWithNoWaiveOfInterest() {
|
||||||
|
// setup
|
||||||
|
installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars);
|
||||||
|
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
|
||||||
|
LocalDate latePaymentDate = firstInstallment.getDueDate().plusMonths(5);
|
||||||
|
|
||||||
|
Money amount = new MoneyBuilder().with(usDollars).with("3600.00").build();
|
||||||
|
LoanTransaction lateRepaymentTransaction = new LoanTransactionBuilder().repayment().with(latePaymentDate).with(amount).build();
|
||||||
|
|
||||||
|
// execute test
|
||||||
|
processor.handleTransaction(lateRepaymentTransaction, usDollars, installments);
|
||||||
|
|
||||||
|
// verification of derived fields on installments
|
||||||
|
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
|
||||||
|
|
||||||
|
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
|
||||||
|
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), secondInstallment, usDollars);
|
||||||
|
|
||||||
|
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
|
||||||
|
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
|
||||||
|
|
||||||
|
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("3000.00"), interestOf("600.00"), interestWaivedOf("0.00"), lateRepaymentTransaction, usDollars);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf(
|
private void assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf(
|
||||||
final String principal, final String interest, final LoanTransaction transaction) {
|
final String principal, final String interest, final LoanTransaction transaction) {
|
||||||
Reference in New Issue
Block a user