diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AdhikarLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AdhikarLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..b3909bf20 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AdhikarLoanRepaymentScheduleTransactionProcessor.java @@ -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 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 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 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 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 + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java index 1b44e547f..e2a654910 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java @@ -85,25 +85,29 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom 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; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java index d9e289999..3e60058dd 100644 --- a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java @@ -48,4 +48,9 @@ public class LoanRepaymentScheduleInstallmentBuilder { this.completed = true; return this; } + + public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) { + this.installmentNumber = withInstallmentNumber; + return this; + } } diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanScheduleTestDataHelper.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanScheduleTestDataHelper.java index 67189bace..0a0e09e8e 100644 --- a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanScheduleTestDataHelper.java +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanScheduleTestDataHelper.java @@ -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 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 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")