add start of Adhikar example for processing loan transactions.

This commit is contained in:
Keith Woodlock
2012-07-29 23:08:25 +01:00
parent dfd504cd73
commit 55083d3bb9
5 changed files with 350 additions and 9 deletions
@@ -0,0 +1,143 @@
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;
/**
* Adhikar style {@link LoanRepaymentScheduleTransactionProcessor}.
*
* From https://mifosforge.jira.com/browse/MIFOS-5636:
*
* Per RBI regulations, all interest must be paid (both current and overdue) before principal is paid.
*
* For example on a loan with two installments due (one current and one overdue) of 220 each (200 principal + 20 interest):
*
* Partial Payment of 40
* 20 Payment to interest on Installment #1 (200 principal remaining)
* 20 Payment to interest on Installment #2 (200 principal remaining)
*/
public class AdhikarLoanRepaymentScheduleTransactionProcessor 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) {
// pay of overdue and current interest due given transaction date
MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (
installment.isInterestDue(currency) &&
(installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))
) {
Money interestPortion = installment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
Money interestWaivedPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion);
}
}
// With whatever is remaining, pay off principal components of installments
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) {
Money principalPortion = installment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestPortion = Money.zero(currency);
Money interestWaivedPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion);
}
}
return transactionAmountRemaining;
}
private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate, List<LoanRepaymentScheduleInstallment> installments) {
LoanRepaymentScheduleInstallment nearest = installments.get(0);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isAfter(transactionDate) || installment.getDueDate().isEqual(transactionDate)) {
nearest = installment;
break;
}
}
return nearest;
}
/**
* 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
}
}
@@ -85,25 +85,29 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return new LocalDate(this.dueDate);
}
public Money getPrincipal(MonetaryCurrency currency) {
public Money getPrincipal(final MonetaryCurrency currency) {
return Money.of(currency, this.principal);
}
public Money getPrincipalCompleted(MonetaryCurrency currency) {
public Money getPrincipalCompleted(final MonetaryCurrency currency) {
return Money.of(currency, this.principalCompleted);
}
public Money getInterest(MonetaryCurrency currency) {
public Money getInterest(final MonetaryCurrency currency) {
return Money.of(currency, this.interest);
}
public Money getInterestCompleted(MonetaryCurrency currency) {
public Money getInterestCompleted(final MonetaryCurrency currency) {
return Money.of(currency, this.interestCompleted);
}
public Money getInterestWaived(MonetaryCurrency currency) {
public Money getInterestWaived(final MonetaryCurrency currency) {
return Money.of(currency, this.interestWaived);
}
public boolean isInterestDue(final MonetaryCurrency currency) {
return getInterest(currency).minus(getInterestCompleted(currency).plus(getInterestWaived(currency))).isGreaterThanZero();
}
public Money getTotal(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterest(currency));
@@ -204,4 +208,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return waivedInterestPortionOfTransaction;
}
public boolean isOverdueOn(final LocalDate transactionDate) {
return this.getDueDate().isBefore(transactionDate);
}
}
@@ -0,0 +1,154 @@
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 AdhikarLoanRepaymentScheduleTransactionProcessorTest {
// class under test
private AdhikarLoanRepaymentScheduleTransactionProcessor 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 AdhikarLoanRepaymentScheduleTransactionProcessor();
}
/**
* Scenario 1: Given no overdue installments, current interest due is paid before principal.
*/
@Test
public void givenNoOverdueInstallmentsOnTimeRepaymentPaysOffInterestDueFirst() {
// setup
Money repaymentAmount = new MoneyBuilder().with(usDollars).with("100.00").build();
LoanTransaction onTimePartialRepayment = new LoanTransactionBuilder().repayment().with(july2nd).with(repaymentAmount).build();
// execute test
processor.handleTransaction(onTimePartialRepayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), interestWaivedOf("0.00"), onTimePartialRepayment, usDollars);
}
/**
* Scenario 2: Given overdue interest exists on overdue installment, a late
* repayment results in overdue interest and current interest being paid
* before principal component for overdue or current installments.
*/
@Test
public void givenOverdueInterestOnOverdueInstallmentsLateRepaymentPaysOffOverdueInterestAndCurrentInterestDue() {
// setup
Money repaymentAmount = new MoneyBuilder().with(usDollars).with("300.00").build();
LoanTransaction lateRepayment = new LoanTransactionBuilder().repayment().with(july2nd.plusMonths(1)).with(repaymentAmount).build();
// execute test
processor.handleTransaction(lateRepayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("200.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), interestWaivedOf("0.00"), secondInstallment, usDollars);
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("300.00"), interestWaivedOf("0.00"), lateRepayment, usDollars);
}
/**
* Scenario 3: Given an overdue installment with overdue interest and principal, a late
* repayment results in overdue interest and current interest being paid
* before principal component for overdue or current installments.
*/
@Test
public void givenAFullyOverdueInstallmentALateRepaymentShouldPayOffOverdueInterestAndCurrentInterestDueFirstThenOverduePrincipal() {
// setup
Money repaymentAmount = new MoneyBuilder().with(usDollars).with("900.00").build();
LoanTransaction lateRepayment = new LoanTransactionBuilder().repayment().with(july2nd.plusMonths(1)).with(repaymentAmount).build();
// execute test
processor.handleTransaction(lateRepayment, usDollars, installments);
// verification of derived fields on installments
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("500.00"), interestOf("200.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("200.00"), interestWaivedOf("0.00"), secondInstallment, usDollars);
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("500.00"), interestOf("400.00"), interestWaivedOf("0.00"), lateRepayment, usDollars);
}
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;
}
}
@@ -48,4 +48,9 @@ public class LoanRepaymentScheduleInstallmentBuilder {
this.completed = true;
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) {
this.installmentNumber = withInstallmentNumber;
return this;
}
}
@@ -11,20 +11,35 @@ import org.mifosng.platform.currency.domain.MonetaryCurrency;
*/
public class LoanScheduleTestDataHelper {
/**
* Creates brand new three installment loan:
*
* For example:
* with firstDueDate = 02 July 2011
*
* Date Principal Interest Interest Waived
* ==================================================================================
* 02 July 2011 1,000 200 0
* 02 August 2011 1,000 200 0
* 02 September 2011 1,000 200 0
*/
public static List<LoanRepaymentScheduleInstallment> createSimpleLoanSchedule(final LocalDate firstDueDate, final MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withInstallmentNumber(1)
.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();
.withInstallmentNumber(2)
.withDueDate(firstDueDate.plusMonths(1))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withInstallmentNumber(3)
.withDueDate(firstDueDate.plusMonths(2))
.withPrincipal("1000.00")
.withInterest("200.00")
@@ -33,9 +48,23 @@ public class LoanScheduleTestDataHelper {
return Arrays.asList(firstInstallment, secondInstallment, thirdInstallment);
}
/**
* Creates three installment loan with first installment fully completed:
*
* For example:
* with firstDueDate = 02 July 2011
*
* Date Principal Interest Interest Waived Completed
* ================================================================================================================
* 02 July 2011 1,000 200 0 true (principal paid, interest paid)
* 02 August 2011 1,000 200 0 false
* 02 September 2011 1,000 200 0 false
*/
public static List<LoanRepaymentScheduleInstallment> createSimpleLoanScheduleWithFirstInstallmentFullyPaid(final LocalDate firstDueDate, final MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withInstallmentNumber(1)
.withDueDate(firstDueDate)
.withPrincipal("1000.00")
.withInterest("200.00")
@@ -43,12 +72,14 @@ public class LoanScheduleTestDataHelper {
.build();
LoanRepaymentScheduleInstallment secondInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withInstallmentNumber(2)
.withDueDate(firstDueDate.plusMonths(1))
.withPrincipal("1000.00")
.withInterest("200.00")
.build();
LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency)
.withInstallmentNumber(3)
.withDueDate(firstDueDate.plusMonths(2))
.withPrincipal("1000.00")
.withInterest("200.00")