From dfd504cd732449c7a36d8dc22b5978b4d9cc89dd Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Sat, 28 Jul 2012 20:56:46 +0100 Subject: [PATCH] create two types of repayment strategy from abstract template. --- ...RepaymentScheduleTransactionProcessor.java | 140 ++++++++++++ ...RepaymentScheduleTransactionProcessor.java | 102 +++++++++ ...RepaymentScheduleTransactionProcessor.java | 206 ------------------ ...RepaymentScheduleTransactionProcessor.java | 130 +++++++++++ .../mifosng/platform/loan/domain/Loan.java | 6 +- ...ntScheduleTransactionProcessorFactory.java | 24 ++ ...mentScheduleTransactionProcessorTest.java} | 36 ++- 7 files changed, 434 insertions(+), 210 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java delete mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessor.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessorFactory.java rename mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/{DefaultLoanRepaymentScheduleTransactionProcessorTest.java => HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java} (91%) diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..2b3c8d337 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -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 repaymentsOrWaivers, + final MonetaryCurrency currency, + final List 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 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 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 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 installments, + final LocalDate transactionDate, + final Money transactionAmount); + + /** + * For early/'in advance' repayments. + */ + protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment( + final LoanRepaymentScheduleInstallment currentInstallment, + final List 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); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..8930e0da4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -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 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) { + + 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 + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessor.java deleted file mode 100644 index 586632c05..000000000 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessor.java +++ /dev/null @@ -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 repaymentsOrWaivers, - final MonetaryCurrency currency, - final List 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 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 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 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 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 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 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 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; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..27f1e3eba --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -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 installments, + final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed); + } + + @Override + protected boolean isTransactionInAdvanceOfInstallment( + final int currentInstallmentIndex, + final List 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 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) { + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 1b3f4348d..e8365287e 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -132,6 +132,9 @@ public class Loan extends AbstractAuditableCustom { @Transient private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory(); + + @Transient + private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessor = new LoanRepaymentScheduleTransactionProcessorFactory(); public static Loan createNew(final Fund fund, final LoanProduct loanProduct, final Client client, @@ -441,7 +444,8 @@ public class Loan extends AbstractAuditableCustom { LoanTransactionComparator transactionComparator = new LoanTransactionComparator(); Collections.sort(repaymentsOrWaivers, transactionComparator); - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = new DefaultLoanRepaymentScheduleTransactionProcessor(); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(); if (isTransactionChronologicallyLatest && !adjusted) { loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments); } else { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessorFactory.java new file mode 100644 index 000000000..753ed01ea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessorFactory.java @@ -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; + } + +} diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessorTest.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java similarity index 91% rename from mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessorTest.java rename to mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java index 1d563b4f6..cbffa258e 100644 --- a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessorTest.java +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java @@ -14,10 +14,10 @@ import org.mifosng.platform.currency.domain.Money; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class DefaultLoanRepaymentScheduleTransactionProcessorTest { +public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest { // class under test - private DefaultLoanRepaymentScheduleTransactionProcessor processor; + private HeavensFamilyLoanRepaymentScheduleTransactionProcessor processor; // private LocalDate july2nd = new LocalDate(2012, 7, 2); @@ -29,7 +29,7 @@ public class DefaultLoanRepaymentScheduleTransactionProcessorTest { installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars); - processor = new DefaultLoanRepaymentScheduleTransactionProcessor(); + processor = new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(); } /** @@ -326,6 +326,36 @@ public class DefaultLoanRepaymentScheduleTransactionProcessorTest { 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( final String principal, final String interest, final LoanTransaction transaction) {