add start of Adhikar example for processing loan transactions.
This commit is contained in:
+143
@@ -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
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
+154
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
@@ -48,4 +48,9 @@ public class LoanRepaymentScheduleInstallmentBuilder {
|
||||
this.completed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) {
|
||||
this.installmentNumber = withInstallmentNumber;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
+35
-4
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user