diff --git a/mifosng-db/0001a-mifos-platform-base-ddl-schema.sql b/mifosng-db/0001a-mifos-platform-base-ddl-schema.sql index 708e83be0..d490b4b0b 100644 --- a/mifosng-db/0001a-mifos-platform-base-ddl-schema.sql +++ b/mifosng-db/0001a-mifos-platform-base-ddl-schema.sql @@ -325,6 +325,7 @@ CREATE TABLE `portfolio_loan_repayment_schedule` ( `principal_completed_derived` decimal(19,6) DEFAULT NULL, `interest_amount` decimal(19,6) DEFAULT NULL, `interest_completed_derived` decimal(19,6) DEFAULT NULL, + `interest_waived_derived` decimal(19,6) DEFAULT NULL, `completed_derived` bit(1) NOT NULL, `createdby_id` bigint(20) DEFAULT NULL, `created_date` datetime DEFAULT NULL, @@ -342,6 +343,8 @@ CREATE TABLE `portfolio_loan_transaction` ( `contra_id` bigint(20) DEFAULT NULL, `transaction_date` date NOT NULL, `amount` decimal(19,6) NOT NULL, + `principal_portion_derived` decimal(19,6) NOT NULL, + `interest_portion_derived` decimal(19,6) NOT NULL, `createdby_id` bigint(20) DEFAULT NULL, `created_date` datetime DEFAULT NULL, `lastmodified_date` datetime DEFAULT NULL, diff --git a/mifosng-db/0002-mifosng-base-reference-data.sql b/mifosng-db/0002-mifosng-base-reference-data.sql index 0b0525492..e80dac3e4 100644 --- a/mifosng-db/0002-mifosng-base-reference-data.sql +++ b/mifosng-db/0002-mifosng-base-reference-data.sql @@ -8,7 +8,8 @@ VALUES (300, 'Active'), (400, 'Withdrawn by client'), (500, 'Rejected'), -(600, 'Closed'); +(600, 'Closed'), +(700, 'Overpaid'); INSERT INTO `ref_currency` diff --git a/mifosng-db/patches-post-multi-tenant/patch-0006-add-derived-fields-on-loan-transaction.sql b/mifosng-db/patches-post-multi-tenant/patch-0006-add-derived-fields-on-loan-transaction.sql new file mode 100644 index 000000000..a1c83ba1f --- /dev/null +++ b/mifosng-db/patches-post-multi-tenant/patch-0006-add-derived-fields-on-loan-transaction.sql @@ -0,0 +1,12 @@ +-- add derived fields for loan transactions and extra + +ALTER TABLE `portfolio_loan_transaction` +ADD COLUMN `principal_portion_derived` decimal(19,6) NOT NULL DEFAULT 0, +ADD COLUMN `interest_portion_derived` decimal(19,6) NOT NULL DEFAULT 0, +ADD COLUMN `interest_waived_derived` decimal(19,6) NOT NULL DEFAULT 0; + + +ALTER TABLE `portfolio_loan_repayment_schedule` +ADD COLUMN `interest_waived_derived` decimal(19,6) NOT NULL DEFAULT 0; + +INSERT INTO `ref_loan_status` (`id`,`display_name`) VALUES (700, 'Overpaid'); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanAccountSummaryData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanAccountSummaryData.java index 59168a66c..8a867f9c6 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanAccountSummaryData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanAccountSummaryData.java @@ -1,6 +1,5 @@ package org.mifosng.platform.api.data; - public class LoanAccountSummaryData { private MoneyData originalPrincipal; @@ -9,32 +8,42 @@ public class LoanAccountSummaryData { private MoneyData principalPaid; private MoneyData principalOutstanding; private MoneyData interestPaid; + private MoneyData interestWaived; private MoneyData interestOutstanding; private MoneyData totalPaid; + private MoneyData totalWaived; private MoneyData totalOutstanding; private MoneyData totalInArrears; - private MoneyData totalWaived; protected LoanAccountSummaryData() { // } - public LoanAccountSummaryData(MoneyData originalPrincipal, - MoneyData principalPaid, MoneyData principalOutstanding, - MoneyData originalInterest, MoneyData interestPaid, - MoneyData interestOutstanding, MoneyData originalTotal, - MoneyData totalPaid, MoneyData totalOutstanding, MoneyData totalInArrears, MoneyData totalWaived) { + public LoanAccountSummaryData( + MoneyData originalPrincipal, + MoneyData principalPaid, + MoneyData principalOutstanding, + MoneyData originalInterest, + MoneyData interestPaid, + MoneyData interestWaived, + MoneyData interestOutstanding, + MoneyData originalTotal, + MoneyData totalPaid, + MoneyData totalWaived, + MoneyData totalOutstanding, + MoneyData totalInArrears) { this.originalPrincipal = originalPrincipal; this.principalPaid = principalPaid; this.principalOutstanding = principalOutstanding; this.originalInterest = originalInterest; this.interestPaid = interestPaid; + this.interestWaived = interestWaived; this.interestOutstanding = interestOutstanding; this.originalTotal = originalTotal; this.totalPaid = totalPaid; + this.totalWaived = totalWaived; this.totalOutstanding = totalOutstanding; this.totalInArrears = totalInArrears; - this.totalWaived = totalWaived; } public boolean isWaiveAllowed(MoneyData tolerance) { @@ -88,6 +97,14 @@ public class LoanAccountSummaryData { public void setInterestPaid(MoneyData interestPaid) { this.interestPaid = interestPaid; } + + public MoneyData getInterestWaived() { + return interestWaived; + } + + public void setInterestWaived(MoneyData interestWaived) { + this.interestWaived = interestWaived; + } public MoneyData getInterestOutstanding() { return interestOutstanding; @@ -104,6 +121,14 @@ public class LoanAccountSummaryData { public void setTotalPaid(MoneyData totalPaid) { this.totalPaid = totalPaid; } + + public MoneyData getTotalWaived() { + return totalWaived; + } + + public void setTotalWaived(MoneyData totalWaived) { + this.totalWaived = totalWaived; + } public MoneyData getTotalOutstanding() { return totalOutstanding; @@ -120,12 +145,4 @@ public class LoanAccountSummaryData { public void setTotalInArrears(MoneyData totalInArrears) { this.totalInArrears = totalInArrears; } - - public MoneyData getTotalWaived() { - return totalWaived; - } - - public void setTotalWaived(MoneyData totalWaived) { - this.totalWaived = totalWaived; - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanRepaymentPeriodData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanRepaymentPeriodData.java index bfa982b00..24d5fdd27 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanRepaymentPeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanRepaymentPeriodData.java @@ -12,9 +12,11 @@ public class LoanRepaymentPeriodData { private MoneyData principalOutstanding; private MoneyData interest; private MoneyData interestPaid; + private MoneyData interestWaived; private MoneyData interestOutstanding; private MoneyData total; private MoneyData totalPaid; + private MoneyData totalWaived; private MoneyData totalOutstanding; public LoanRepaymentPeriodData() { @@ -24,8 +26,8 @@ public class LoanRepaymentPeriodData { public LoanRepaymentPeriodData(Long loanId, Integer period, LocalDate date, MoneyData principal, MoneyData principalPaid, MoneyData principalOutstanding, MoneyData interest, - MoneyData interestPaid, MoneyData interestOutstanding, - MoneyData total, MoneyData totalPaid, MoneyData totalOutstanding) { + MoneyData interestPaid, MoneyData interestWaived, MoneyData interestOutstanding, + MoneyData total, MoneyData totalPaid, MoneyData totalWaived, MoneyData totalOutstanding) { this.loanId = loanId; this.period = period; this.date = date; @@ -34,9 +36,11 @@ public class LoanRepaymentPeriodData { this.principalOutstanding = principalOutstanding; this.interest = interest; this.interestPaid = interestPaid; + this.interestWaived = interestWaived; this.interestOutstanding = interestOutstanding; this.total = total; this.totalPaid = totalPaid; + this.totalWaived = totalWaived; this.totalOutstanding = totalOutstanding; } @@ -103,6 +107,14 @@ public class LoanRepaymentPeriodData { public void setInterestPaid(MoneyData interestPaid) { this.interestPaid = interestPaid; } + + public MoneyData getInterestWaived() { + return interestWaived; + } + + public void setInterestWaived(MoneyData interestWaived) { + this.interestWaived = interestWaived; + } public MoneyData getInterestOutstanding() { return interestOutstanding; @@ -127,6 +139,14 @@ public class LoanRepaymentPeriodData { public void setTotalPaid(MoneyData totalPaid) { this.totalPaid = totalPaid; } + + public MoneyData getTotalWaived() { + return totalWaived; + } + + public void setTotalWaived(MoneyData totalWaived) { + this.totalWaived = totalWaived; + } public MoneyData getTotalOutstanding() { return totalOutstanding; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java index 24f77bc10..17b2ee623 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java @@ -54,7 +54,7 @@ public class DefaultLoanLifecycleStateMachine implements } break; case LOAN_REPAYMENT: - if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) { newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses); } else { newState = from; @@ -80,6 +80,11 @@ public class DefaultLoanLifecycleStateMachine implements newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); } break; + case LOAN_OVERPAYMENT: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED, LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.OVERPAID, allowedLoanStatuses); + } + break; } return newState; 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 new file mode 100644 index 000000000..e493bc10b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,204 @@ +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(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 + */ + protected void handleLateTransaction( + final List installments, + final LoanTransaction loanTransaction, + final MonetaryCurrency currency) { + + final Money lateRepayment = loanTransaction.getAmount(currency); + Money loanOverPaymentAmount = handleOverpaymentTransaction(installments, loanTransaction, currency, lateRepayment); + 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/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 5eca6b550..bfe003382 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 @@ -2,6 +2,7 @@ package org.mifosng.platform.loan.domain; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -354,10 +355,8 @@ public class Loan extends AbstractAuditableCustom { this.disbursedOnDate = null; } - public void waive(LoanTransaction loanTransaction, - LoanLifecycleStateMachine loanLifecycleStateMachine) { - this.loanStatus = loanLifecycleStateMachine.transition( - LoanEvent.LOAN_REPAYMENT, this.loanStatus); + public void waive(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus); loanTransaction.updateLoan(this); this.loanTransactions.add(loanTransaction); @@ -395,49 +394,23 @@ public class Loan extends AbstractAuditableCustom { waived, getInArrearsTolerance()); } - if (this.isRepaidInFull()) { - this.loanStatus = loanLifecycleStateMachine.transition( - LoanEvent.REPAID_IN_FULL, this.loanStatus); - this.closedOnDate = loanTransaction.getTransactionDate().toDate(); - this.maturedOnDate = loanTransaction.getTransactionDate().toDate(); - - if (isInterestRebateAllowed()) { - Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction - .getTransactionDate()); - if (rebateDue.isGreaterThanZero()) { - this.loanStatus = loanLifecycleStateMachine.transition( - LoanEvent.INTERST_REBATE_OWED, this.loanStatus); - this.interestRebateOwed = rebateDue.getAmount(); - } - } - } + doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine); } public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + handleRepaymentTransaction(loanTransaction, loanLifecycleStateMachine, false); + } + + private void handleRepaymentTransaction( + final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final boolean adjusted) { this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus); loanTransaction.updateLoan(this); + + boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, this.loanTransactions); + this.loanTransactions.add(loanTransaction); - deriveLoanRepaymentScheduleCompletedData(); - -// Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency()); -// MonetaryCurrency currency = totalRepaidOrWaived.getCurrency(); -// -// int repaymentIndex = 0; -// -// for (LoanTransaction transaction : this.loanTransactions) { -// if (transaction.isRepayment() || transaction.isWaiver()) { -// totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction.getAmount()); -// -// LoanRepaymentScheduleInstallment currentInstallment = this.repaymentScheduleInstallments.get(repaymentIndex); -// if (currentInstallment.unpaid()) { -// currentInstallment.payOffWith(transaction, currency); -// } else { -// repaymentIndex++; -// } -// } -// } - if (loanTransaction.isNotRepayment()) { final String errorMessage = "A transaction of type repayment was expected but not received."; throw new InvalidLoanTransactionTypeException("transaction", @@ -458,66 +431,81 @@ public class Loan extends AbstractAuditableCustom { throw new InvalidLoanStateTransitionException("repayment", "cannot.be.a.futre.date", errorMessage, loanTransactionDate); } + + List repaymentsOrWaivers = new ArrayList(); + for (LoanTransaction transaction : this.loanTransactions) { + if (!transaction.isDisbursement() && transaction.isNotContra()) { + repaymentsOrWaivers.add(transaction); + } + } + LoanTransactionComparator transactionComparator = new LoanTransactionComparator(); + Collections.sort(repaymentsOrWaivers, transactionComparator); + + LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = new DefaultLoanRepaymentScheduleTransactionProcessor(); + if (isTransactionChronologicallyLatest && !adjusted) { + loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments); + } else { + loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments); + } + + doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine); + } + private void doPostLoanTransactionChecks(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + + if (this.isOverPaid()) { + handleLoanOverpayment(loanTransaction, loanLifecycleStateMachine); + } + if (this.isRepaidInFull()) { - this.loanStatus = loanLifecycleStateMachine.transition( - LoanEvent.REPAID_IN_FULL, this.loanStatus); - this.closedOnDate = loanTransaction.getTransactionDate().toDate(); - this.maturedOnDate = loanTransaction.getTransactionDate().toDate(); + handleLoanRepaymentInFull(loanTransaction, loanLifecycleStateMachine); + } + } - if (isInterestRebateAllowed()) { - Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction - .getTransactionDate()); - if (rebateDue.isGreaterThanZero()) { - this.loanStatus = loanLifecycleStateMachine.transition( - LoanEvent.INTERST_REBATE_OWED, this.loanStatus); - this.interestRebateOwed = rebateDue.getAmount(); + private void handleLoanRepaymentInFull(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus); + this.closedOnDate = loanTransaction.getTransactionDate().toDate(); + this.maturedOnDate = loanTransaction.getTransactionDate().toDate(); + + if (isInterestRebateAllowed()) { + Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction.getTransactionDate()); + if (rebateDue.isGreaterThanZero()) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus); + this.interestRebateOwed = rebateDue.getAmount(); + } + } + } + + private void handleLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, this.loanStatus); + this.closedOnDate = null; + this.maturedOnDate = null; + } + + private boolean isChronologicallyLatestRepaymentOrWaiver( + final LoanTransaction loanTransaction, + final List loanTransactions) { + + boolean isChronologicallyLatestRepaymentOrWaiver = true; + + LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); + for (LoanTransaction previousTransaction : loanTransactions) { + if (!previousTransaction.isDisbursement() && previousTransaction.isNotContra()) { + if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate()) || + currentTransactionDate.isEqual(previousTransaction.getTransactionDate())) { + isChronologicallyLatestRepaymentOrWaiver = false; + break; } } } - } - - private void deriveLoanRepaymentScheduleCompletedData() { - - Money totalRepaidOrWaivedAgainstLoan = calculateTotalPaidOrWaived(); - - Money remainingToPayoffAgainstLoanSchedule = totalRepaidOrWaivedAgainstLoan; - Money totalOverpaid = Money.zero(totalRepaidOrWaivedAgainstLoan.getCurrency()); - int repaymentInstallmentIndex = 0; - while (remainingToPayoffAgainstLoanSchedule.isGreaterThanZero()) { - - if (repaymentInstallmentIndex == this.repaymentScheduleInstallments.size()) { - totalOverpaid = remainingToPayoffAgainstLoanSchedule; - - // to exit while loop - remainingToPayoffAgainstLoanSchedule = remainingToPayoffAgainstLoanSchedule.minus(totalOverpaid); - } else { - LoanRepaymentScheduleInstallment scheduledRepaymentInstallment = this.repaymentScheduleInstallments.get(repaymentInstallmentIndex); - remainingToPayoffAgainstLoanSchedule = scheduledRepaymentInstallment.updateDerivedComponents(remainingToPayoffAgainstLoanSchedule); - repaymentInstallmentIndex++; - } - } - } - - private Money calculateTotalPaidOrWaived() { - - Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - - for (LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepayment() || transaction.isWaiver()) { - totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction - .getAmount()); - } - } - - return totalRepaidOrWaived; + + return isChronologicallyLatestRepaymentOrWaiver; } public LocalDate possibleNextRepaymentDate() { LocalDate earliestUnpaidInstallmentDate = new LocalDate(); for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (installment.unpaid()) { + if (installment.isNotFullyCompleted()) { earliestUnpaidInstallmentDate = installment.getDueDate(); break; } @@ -545,7 +533,7 @@ public class Loan extends AbstractAuditableCustom { Money possibleNextRepaymentAmount = Money.zero(currency); for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (installment.unpaid()) { + if (installment.isNotFullyCompleted()) { possibleNextRepaymentAmount = installment.getTotalDue(currency); break; } @@ -568,7 +556,7 @@ public class Loan extends AbstractAuditableCustom { transactionForAdjustment.contra(); if (newTransactionDetail.isRepayment()) { - makeRepayment(newTransactionDetail, loanLifecycleStateMachine); + handleRepaymentTransaction(newTransactionDetail, loanLifecycleStateMachine, true); } if (newTransactionDetail.isWaiver()) { @@ -577,39 +565,42 @@ public class Loan extends AbstractAuditableCustom { } private boolean isRepaidInFull() { + + MonetaryCurrency currency = loanCurrency(); - Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - Money cumulativeTotal = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - Money cumulativeWaived = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); - + Money cumulativeOriginalExpectedTotal = Money.zero(currency); + Money cumulativeTotalPaid = Money.zero(currency); + Money cumulativeTotalWaived = Money.zero(currency); + for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { - cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment - .getPrincipal(loanCurrency())); - cumulativeInterest = cumulativeInterest.plus(scheduledRepayment - .getInterest(loanCurrency())); - cumulativeTotal = cumulativeTotal.plus(scheduledRepayment - .getTotal(loanCurrency())); + cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency)); + cumulativeTotalPaid = cumulativeTotalPaid.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency))); + cumulativeTotalWaived = cumulativeTotalWaived.plus(scheduledRepayment.getInterestWaived(currency)); } + + Money totalOutstanding = cumulativeOriginalExpectedTotal.minus(cumulativeTotalPaid.plus(cumulativeTotalWaived)); + boolean isRepaidInFull = totalOutstanding.isZero(); + + return isRepaidInFull; + } + + private boolean isOverPaid() { + + Money totalPaidInRepayments = getTotalPaidInRepayments(); + + MonetaryCurrency currency = loanCurrency(); - for (LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepayment()) { - cumulativePaid = cumulativePaid.plus(transaction.getAmount()); - } - if (transaction.isWaiver()) { - cumulativeWaived = cumulativeWaived.plus(transaction - .getAmount()); - } + Money cumulativeOriginalExpectedTotal = Money.zero(currency); + Money cumulativeTotalPaidOnInstallments = Money.zero(currency); + Money cumulativeTotalWaivedOnInstallments = Money.zero(currency); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { + cumulativeOriginalExpectedTotal = cumulativeOriginalExpectedTotal.plus(scheduledRepayment.getPrincipal(currency)); + cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestCompleted(currency))); + cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency)); } - - return cumulativePaid.plus(cumulativeWaived).isGreaterThanOrEqualTo( - cumulativeTotal); + + return totalPaidInRepayments.isGreaterThan(cumulativeTotalPaidOnInstallments); } private MonetaryCurrency loanCurrency() { @@ -914,7 +905,7 @@ public class Loan extends AbstractAuditableCustom { LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate); - Money totalPaidToDate = this.getTotalPaid(); + Money totalPaidToDate = this.getTotalPaidInRepayments(); Money totalOutstandingBasedOnExpectedMaturityDate = this .getTotalOutstanding(); @@ -937,13 +928,11 @@ public class Loan extends AbstractAuditableCustom { } public Money getTotalOutstanding() { - return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus( - getTotalPaid()); + return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(getTotalPaidInRepayments()); } - private Money getTotalPaid() { - Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail - .getPrincipal().getCurrency()); + private Money getTotalPaidInRepayments() { + Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency()); for (LoanTransaction repayment : this.loanTransactions) { if (repayment.isRepayment()) { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java index 759cf75b1..2c257140b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java @@ -4,5 +4,5 @@ public enum LoanEvent { LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT, // - REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED; + REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED, LOAN_OVERPAYMENT; } 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 f8f340a03..1b44e547f 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 @@ -29,16 +29,19 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom installments); + + void handleTransaction(List repaymentsOrWaivers, MonetaryCurrency currency, List repaymentScheduleInstallments); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java index e5b12253f..e6106f6bf 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java @@ -17,6 +17,7 @@ public class LoanStatus implements Persistable { public static final Integer WITHDRAWN_BY_CLIENT = 400; public static final Integer REJECTED = 500; public static final Integer CLOSED = 600; + public static final Integer OVERPAID = 700; @Id @Column(name = "id") diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java index f832f7434..aa25f5902 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java @@ -37,6 +37,15 @@ public class LoanTransaction extends AbstractAuditableCustom { @Column(name = "amount", scale = 6, precision = 19, nullable = false) private final BigDecimal amount; + @Column(name = "principal_portion_derived", scale = 6, precision = 19, nullable = false) + private BigDecimal principalPortion = BigDecimal.ZERO; + + @Column(name = "interest_portion_derived", scale = 6, precision = 19, nullable = false) + private BigDecimal interestPortion = BigDecimal.ZERO; + + @Column(name = "interest_waived_derived", scale = 6, precision = 19) + private BigDecimal interestWaivedPortion = BigDecimal.ZERO; + @Temporal(TemporalType.DATE) @Column(name = "transaction_date", nullable=false) private final Date dateOf; @@ -155,4 +164,36 @@ public class LoanTransaction extends AbstractAuditableCustom { public boolean isNonZero() { return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0; } + + /** + * This updates the derived fields of a loan transaction for the principal, interest and interest waived portions. + * + * This accumulates the values passed to the already existent values for each of the portions. + */ + public void updateComponents(final Money principal, final Money interest, final Money interestWaivedPortion) { + MonetaryCurrency currency = principal.getCurrency(); + this.principalPortion = getPrincipalPortion(currency).plus(principal).getAmount(); + this.interestPortion = getInterestPortion(currency).plus(interest).getAmount(); + this.interestWaivedPortion = getInterestWaivedPortion(currency).plus(interestWaivedPortion).getAmount(); + } + + public Money getPrincipalPortion(MonetaryCurrency currency) { + return Money.of(currency, principalPortion); + } + + public Money getInterestPortion(MonetaryCurrency currency) { + return Money.of(currency, interestPortion); + } + + public Money getInterestWaivedPortion(MonetaryCurrency currency) { + if (this.interestWaivedPortion == null) { + this.interestWaivedPortion = BigDecimal.ZERO; + } + return Money.of(currency, interestWaivedPortion); + } + + public void resetDerivedComponents() { + this.principalPortion = BigDecimal.ZERO; + this.interestPortion = BigDecimal.ZERO; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java new file mode 100644 index 000000000..57bf43267 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java @@ -0,0 +1,15 @@ +package org.mifosng.platform.loan.domain; + +import java.util.Comparator; + +/** + * + */ +public class LoanTransactionComparator implements Comparator { + + @Override + public int compare(final LoanTransaction o1, final LoanTransaction o2) { + return o1.getTransactionDate().compareTo(o2.getTransactionDate()); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/TransactionProccessingResult.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/TransactionProccessingResult.java new file mode 100644 index 000000000..063a07597 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/TransactionProccessingResult.java @@ -0,0 +1,22 @@ +package org.mifosng.platform.loan.domain; + +import org.mifosng.platform.currency.domain.Money; + +public class TransactionProccessingResult { + + private final Money overPaymentAmount; + private final boolean overPayment; + + public TransactionProccessingResult(final Money overPaymentAmount, final boolean overPayment) { + this.overPaymentAmount = overPaymentAmount; + this.overPayment = overPayment; + } + + public Money getOverPaymentAmount() { + return overPaymentAmount; + } + + public boolean isOverPayment() { + return overPayment; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java index 717f05d8f..7e9b9df4e 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java @@ -122,60 +122,66 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { BigDecimal principalOutstanding = BigDecimal.ZERO; BigDecimal originalInterest = BigDecimal.ZERO; BigDecimal interestPaid = BigDecimal.ZERO; + BigDecimal interestWaived = BigDecimal.ZERO; BigDecimal interestOutstanding = BigDecimal.ZERO; BigDecimal originalTotal = BigDecimal.ZERO; BigDecimal totalPaid = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - BigDecimal totalInArrears = BigDecimal.ZERO; BigDecimal totalWaived = BigDecimal.ZERO; + BigDecimal totalOutstanding = BigDecimal.ZERO; + + BigDecimal totalInArrears = BigDecimal.ZERO; for (LoanRepaymentPeriodData installment : repaymentSchedule) { - originalPrincipal = originalPrincipal.add(installment - .getPrincipal().getAmount()); - principalPaid = principalPaid.add(installment.getPrincipalPaid() - .getAmount()); - principalOutstanding = principalOutstanding.add(installment - .getPrincipalOutstanding().getAmount()); + originalPrincipal = originalPrincipal.add(installment.getPrincipal().getAmount()); + principalPaid = principalPaid.add(installment.getPrincipalPaid().getAmount()); + principalOutstanding = principalOutstanding.add(installment.getPrincipalOutstanding().getAmount()); - originalInterest = originalInterest.add(installment.getInterest() - .getAmount()); - interestPaid = interestPaid.add(installment.getInterestPaid() - .getAmount()); - interestOutstanding = interestOutstanding.add(installment - .getInterestOutstanding().getAmount()); + originalInterest = originalInterest.add(installment.getInterest().getAmount()); + interestPaid = interestPaid.add(installment.getInterestPaid().getAmount()); + interestWaived = interestWaived.add(installment.getInterestWaived().getAmount()); + interestOutstanding = interestOutstanding.add(installment.getInterestOutstanding().getAmount()); - originalTotal = originalTotal.add(installment.getTotal() - .getAmount()); + originalTotal = originalTotal.add(installment.getTotal().getAmount()); totalPaid = totalPaid.add(installment.getTotalPaid().getAmount()); - totalOutstanding = totalOutstanding.add(installment - .getTotalOutstanding().getAmount()); + totalWaived = totalWaived.add(installment.getTotalWaived().getAmount()); + totalOutstanding = totalOutstanding.add(installment.getTotalOutstanding().getAmount()); if (installment.getDate().isBefore(new LocalDate())) { - totalInArrears = totalInArrears.add(installment - .getTotalOutstanding().getAmount()); + totalInArrears = totalInArrears.add(installment.getTotalOutstanding().getAmount()); } } + // sum up all repayment transactions Long waiverType = (long) 4; + MoneyData totalRepaymentAmount = MoneyData.of(currencyData, BigDecimal.ZERO); for (LoanRepaymentTransactionData loanRepayment : loanRepayments) { Long transactionType = loanRepayment.getTransactionType().getId(); if (transactionType.equals(waiverType)) { - totalWaived = totalWaived.add(loanRepayment.getTotal() - .getAmount()); + // skip + } else { + totalRepaymentAmount = totalRepaymentAmount.plus(loanRepayment.getTotal()); } - + } + + // detect if loan is in overpaid state + if (totalRepaymentAmount.isGreaterThan(MoneyData.of(currencyData, totalPaid))) { + BigDecimal difference = totalRepaymentAmount.getAmount().subtract(totalPaid); + totalOutstanding = totalOutstanding.subtract(difference); } - return new LoanAccountSummaryData(MoneyData.of(currencyData, - originalPrincipal), MoneyData.of(currencyData, principalPaid), - MoneyData.of(currencyData, principalOutstanding), MoneyData.of( - currencyData, originalInterest), MoneyData.of( - currencyData, interestPaid), MoneyData.of(currencyData, - interestOutstanding), MoneyData.of(currencyData, - originalTotal), MoneyData.of(currencyData, totalPaid), - MoneyData.of(currencyData, totalOutstanding), MoneyData.of( - currencyData, totalInArrears), MoneyData.of( - currencyData, totalWaived)); + return new LoanAccountSummaryData( + MoneyData.of(currencyData,originalPrincipal), + MoneyData.of(currencyData, principalPaid), + MoneyData.of(currencyData, principalOutstanding), + MoneyData.of(currencyData, originalInterest), + MoneyData.of(currencyData, interestPaid), + MoneyData.of(currencyData, interestWaived), + MoneyData.of(currencyData, interestOutstanding), + MoneyData.of(currencyData, originalTotal), + MoneyData.of(currencyData, totalPaid), + MoneyData.of(currencyData, totalWaived), + MoneyData.of(currencyData, totalOutstanding), + MoneyData.of(currencyData, totalInArrears)); } @Override @@ -524,7 +530,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { return " ls.loan_id as loanId, ls.installment as period, ls.duedate as `date`, " + " ls.principal_amount as principal, ls.principal_completed_derived as principalPaid, " - + " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, " + + " ls.interest_amount as interest, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, " + " l.currency_code as currencyCode, l.currency_digits as currencyDigits, rc.`name` as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode " + " from portfolio_loan l " + " join portfolio_loan_repayment_schedule ls on ls.loan_id = l.id " @@ -553,29 +559,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { BigDecimal principalBD = rs.getBigDecimal("principal"); MoneyData principal = MoneyData.of(currencyData, principalBD); BigDecimal principalPaidBD = rs.getBigDecimal("principalPaid"); - MoneyData principalPaid = MoneyData.of(currencyData, - principalPaidBD); - MoneyData principalOutstanding = MoneyData.of(currencyData, - principalBD.subtract(principalPaidBD)); + MoneyData principalPaid = MoneyData.of(currencyData, principalPaidBD); + MoneyData principalOutstanding = MoneyData.of(currencyData,principalBD.subtract(principalPaidBD)); BigDecimal interestBD = rs.getBigDecimal("interest"); MoneyData interest = MoneyData.of(currencyData, interestBD); BigDecimal interestPaidBD = rs.getBigDecimal("interestPaid"); MoneyData interestPaid = MoneyData.of(currencyData, interestPaidBD); - MoneyData interestOutstanding = MoneyData.of(currencyData, - interestBD.subtract(interestPaidBD)); + + BigDecimal interestWaivedBD = rs.getBigDecimal("interestWaived"); + if (interestWaivedBD == null) { + interestWaivedBD = BigDecimal.ZERO; + } + MoneyData interestWaived = MoneyData.of(currencyData, interestWaivedBD); + MoneyData interestOutstanding = MoneyData.of(currencyData, interestBD.subtract(interestPaidBD).subtract(interestWaivedBD)); - MoneyData total = MoneyData.of(currencyData, - principalBD.add(interestBD)); - MoneyData totalPaid = MoneyData.of(currencyData, - principalPaidBD.add(interestPaidBD)); - MoneyData totalOutstanding = MoneyData.of(currencyData, - principalBD.add(interestBD).subtract(principalPaidBD) - .subtract(interestPaidBD)); + MoneyData total = MoneyData.of(currencyData,principalBD.add(interestBD)); + MoneyData totalPaid = principalPaid.plus(interestPaid); + MoneyData totalWaived = interestWaived; + MoneyData totalOutstanding = principalOutstanding.plus(interestOutstanding); return new LoanRepaymentPeriodData(loanId, period, date, principal, principalPaid, principalOutstanding, interest, - interestPaid, interestOutstanding, total, totalPaid, + interestPaid, interestWaived, interestOutstanding, total, totalPaid, totalWaived, totalOutstanding); } } 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/DefaultLoanRepaymentScheduleTransactionProcessorTest.java new file mode 100644 index 000000000..1d563b4f6 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DefaultLoanRepaymentScheduleTransactionProcessorTest.java @@ -0,0 +1,392 @@ +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 DefaultLoanRepaymentScheduleTransactionProcessorTest { + + // class under test + private DefaultLoanRepaymentScheduleTransactionProcessor processor; + + // + private LocalDate july2nd = new LocalDate(2012, 7, 2); + private MonetaryCurrency usDollars = new MonetaryCurrencyBuilder().withCode("USD").withDigitsAfterDecimal(2).build(); + private List installments; + + @Before + public void setUpForEachTestCase() { + + installments = LoanScheduleTestDataHelper.createSimpleLoanSchedule(july2nd, usDollars); + + processor = new DefaultLoanRepaymentScheduleTransactionProcessor(); + } + + /** + * Scenario 1: Single transaction which is less than the interest component of the first installment. + * + * Expectation: + * - First installment shows interest completed equal to that of transactions, zero principal completed. (payment order interest, principal) + * - transaction has interest portion equal to transaction amount, and zero principal portion + */ + @Test + public void givenSingleOnTimeLoanTransactionShouldPayoffInterestComponentFirst() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money oneHundredDollars = new MoneyBuilder().with(usDollars).with("100.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(oneHundredDollars).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", singleInstallment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", onTimePartialPayment); + } + + /** + * Scenario 2: Two transactions which is less than the interest component of the first installment. + * + * Expectation: + * - First installment shows interest completed equal to that of transactions, zero principal completed. (payment order interest, principal) + * - transactions has interest portion equal to transaction amount, and zero principal portion + */ + @Test + public void givenMultipleOnTimeLoanTransactionShouldPayoffInterestComponentFirst() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money oneHundredDollars = new MoneyBuilder().with(usDollars).with("100.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(oneHundredDollars).build(); + + Money fiftyDollars = new MoneyBuilder().with(usDollars).with("50.00").build(); + LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(fiftyDollars).build(); + + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("0.00", "150.00", singleInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "100.00", onTimePartialPayment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("0.00", "50.00", secondOnTimePartialPayment); + } + + /** + * Scenario 3: Single transaction which is greater than the interest component of the first installment but less than the total due for installment. + * + * Expectation: + * - First installment shows interest completed equal to that interest due, remainder is against principal completed. (payment order interest, principal) + * - transaction has interest portion equal to interest expected and principal portion of remainder. + */ + @Test + public void givenSingleOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponents() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money amount = new MoneyBuilder().with(usDollars).with("300.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", singleInstallment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", onTimePartialPayment); + } + + /** + * Scenario 4: Single transaction which is greater than the interest component of the first installment but less than the total due for installment. + * + * Expectation: + * - First installment shows interest completed equal to that interest due, remainder is against principal completed. (payment order interest, principal) + * - transaction has interest portion equal to interest expected and principal portion of remainder. + */ + @Test + public void givenMultipleOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponents() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money amount = new MoneyBuilder().with(usDollars).with("300.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("50.00").build(); + LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("150.00", "200.00", singleInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("100.00", "200.00", onTimePartialPayment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("50.00", "0.00", secondOnTimePartialPayment); + } + + /** + * Scenario 5:Single transaction which is equal to the total due for installment. + * + * Expectation: + * - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal) + * - transaction has interest portion and principal portions + */ + @Test + public void givenSingleFullOnTimeLoanTransactionShouldPayoffInterestThenPrincipalComponentInFull() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money amount = new MoneyBuilder().with(usDollars).with("1200.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", onTimePartialPayment); + } + + /** + * Scenario 6: Multiple transactions which together equal to the total due for installment. + * + * Expectation: + * - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal) + * - transaction has interest portion and principal portions + */ + @Test + public void givenMuliptleOnTimeLoanTransactionsThatInFullShouldPayoffInterestThenPrincipalComponentInFull() { + + // setup + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + + Money amount = new MoneyBuilder().with(usDollars).with("600.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("600.00").build(); + LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("400.00", "200.00", onTimePartialPayment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("600.00", "0.00", secondOnTimePartialPayment); + } + + /** + * Scenario 7: Single transaction which is greater than total due for first installment. + * + * Expectation: + * - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal) + * - Second installment shows principal completed equal to the remaining amount + * - transaction has interest portion and principal portions, the principal portion is the sum of principal completed for first and second installments + */ + @Test + public void givenSingleOnTimeLoanTransactionThatOverpaysInstallmentShouldPayoffInterestThenPrincipalComponentInFullOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfNextInstallment() { + + // setup + Money amount = new MoneyBuilder().with(usDollars).with("1300.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", firstInstallment); + + LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "0.00", secondInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("1100.00", "200.00", onTimePartialPayment); + } + + /** + * Scenario 8: Multiple transactions which together are greater than total due for first installment. + * + * Expectation: + * - First installment shows interest completed and principal completed equal to their due amounts. (payment order interest, principal) + * - Second installment shows principal completed equal to the remaining amount + */ + @Test + public void givenMultipleOnTimeLoanTransactionThatOverpaysInstallmentShouldPayoffInterestThenPrincipalComponentInFullOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfNextInstallment() { + + // setup + Money amount = new MoneyBuilder().with(usDollars).with("600.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + Money secondTransactionAmount = new MoneyBuilder().with(usDollars).with("700.00").build(); + LoanTransaction secondOnTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(secondTransactionAmount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + processor.handleTransaction(secondOnTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + LoanRepaymentScheduleInstallment singleInstallment = this.installments.get(0); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("1000.00", "200.00", singleInstallment); + + LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("100.00", "0.00", secondInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("400.00", "200.00", onTimePartialPayment); + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "0.00", secondOnTimePartialPayment); + } + + /** + * Scenario 9: Single transaction which overpays first installment and second installment with outstanding amount been account as principal in third installment. + * + * Where an installments principal is paid for fully in advance - the interest component is waived. + */ + @Test + public void givenOverPaymentShouldPayoffInterestThenPrincipalComponentsOfFirstInstallmentWithRemainingOverpaymentAgainstPrincipalOfAllSubsequentInstallments() { + // setup + Money amount = new MoneyBuilder().with(usDollars).with("3000.00").build(); + LoanTransaction onTimePartialPayment = new LoanTransactionBuilder().repayment().with(july2nd).with(amount).build(); + + // execute test + processor.handleTransaction(onTimePartialPayment, usDollars, installments); + + // verification of derived fields on installments + LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0); + assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), firstInstallment, usDollars); + + LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1); + assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("0.00"), interestWaivedOf("200.00"), secondInstallment, usDollars); + + LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2); + assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("800.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars); + + assertThatLoanTransactionHasDerivedFieldsOf(principalOf("2800.00"), interestOf("200.00"), interestWaivedOf("200.00"), onTimePartialPayment, usDollars); + } + + /** + * Scenario 10: Single transaction which is a repayment in advance + */ + @Test + public void givenRepaymentOfInstallmentInAdvanceShouldPayoffOnlyPrincipalComponent() { + // setup + installments = LoanScheduleTestDataHelper.createSimpleLoanScheduleWithFirstInstallmentFullyPaid(july2nd, usDollars); + LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0); + LocalDate paymentInAdvanceDate = firstInstallment.getDueDate().minusDays(1); + + Money amount = new MoneyBuilder().with(usDollars).with("900.00").build(); + LoanTransaction paymentInAdvance = new LoanTransactionBuilder().repayment().with(paymentInAdvanceDate).with(amount).build(); + + // execute test + processor.handleTransaction(paymentInAdvance, usDollars, installments); + + // verification of derived fields on installments + LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("900.00", "0.00", secondInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("900.00", "0.00", paymentInAdvance); + } + + /** + * Scenario 11: Single transaction which is a repayment many days early but still does not qualify for 'in advance'. + */ + @Test + public void givenRepaymentOfInstallmentBeforeDueDateButNotInAdvanceShouldPayoffInterestAndPrincipalComponents() { + // setup + installments = LoanScheduleTestDataHelper.createSimpleLoanScheduleWithFirstInstallmentFullyPaid(july2nd, usDollars); + LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0); + LocalDate paymentBeforeDueDateButNotInAdvanceDate = firstInstallment.getDueDate().plusDays(1); + + Money amount = new MoneyBuilder().with(usDollars).with("900.00").build(); + LoanTransaction paymentBeforeDueDateButNotInAdvance = new LoanTransactionBuilder().repayment().with(paymentBeforeDueDateButNotInAdvanceDate).with(amount).build(); + + // execute test + processor.handleTransaction(paymentBeforeDueDateButNotInAdvance, usDollars, installments); + + // verification of derived fields on installments + LoanRepaymentScheduleInstallment secondInstallment = this.installments.get(1); + assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", secondInstallment); + + assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf("700.00", "200.00", paymentBeforeDueDateButNotInAdvance); + } + + private void assertThatLoanTransactionHasDerivedFieldsForPrincipalInterestOf( + final String principal, final String interest, final LoanTransaction transaction) { + + Money expectedInterestPortion = new MoneyBuilder().with(usDollars).with(interest).build(); + assertThat(transaction.getInterestPortion(usDollars).toString(), is(expectedInterestPortion.toString())); + + Money expectedPrincipalPortion = new MoneyBuilder().with(usDollars).with(principal).build(); + assertThat(transaction.getPrincipalPortion(usDollars).toString(), is(expectedPrincipalPortion.toString())); + } + + private void assertThatLoanInstallmentHasDerivedFieldsForPrincipalInterestOf(final String principal, final String interest, + final LoanRepaymentScheduleInstallment installment) { + Money expectedInterestCompleted = new MoneyBuilder().with(usDollars).with(interest).build(); + assertThat(installment.getInterestCompleted(usDollars).toString(), is(expectedInterestCompleted.toString())); + + Money expectedPrincipalCompleted = new MoneyBuilder().with(usDollars).with(principal).build(); + assertThat(installment.getPrincipalCompleted(usDollars).toString(), is(expectedPrincipalCompleted.toString())); + } + + 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 e5c045c03..d9e289999 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 @@ -8,11 +8,11 @@ public class LoanRepaymentScheduleInstallmentBuilder { private Loan loan = null; private Integer installmentNumber = Integer.valueOf(1); -// private LocalDate from = LocalDate.now(); private LocalDate dueDate = LocalDate.now(); private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); private Money principal = new MoneyBuilder().build(); private Money interest = new MoneyBuilder().build(); + private boolean completed = false; public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) { this.currencyDetail = currencyDetail; @@ -21,7 +21,12 @@ public class LoanRepaymentScheduleInstallmentBuilder { } public LoanRepaymentScheduleInstallment build() { - return new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount()); + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, dueDate, principal.getAmount(), interest.getAmount()); + if (completed) { + installment.payPrincipalComponent(principal); + installment.payInterestComponent(interest); + } + return installment; } public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) { @@ -38,4 +43,9 @@ public class LoanRepaymentScheduleInstallmentBuilder { this.dueDate = withDueDate; return this; } + + public LoanRepaymentScheduleInstallmentBuilder completed() { + this.completed = true; + 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 new file mode 100644 index 000000000..67189bace --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanScheduleTestDataHelper.java @@ -0,0 +1,60 @@ +package org.mifosng.platform.loan.domain; + +import java.util.Arrays; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; + +/** + * Helper class for creating loan schedule data suitable for testing. + */ +public class LoanScheduleTestDataHelper { + + public static List createSimpleLoanSchedule(final LocalDate firstDueDate, final MonetaryCurrency currency) { + LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency) + .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(); + + LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency) + .withDueDate(firstDueDate.plusMonths(2)) + .withPrincipal("1000.00") + .withInterest("200.00") + .build(); + + return Arrays.asList(firstInstallment, secondInstallment, thirdInstallment); + } + + public static List createSimpleLoanScheduleWithFirstInstallmentFullyPaid(final LocalDate firstDueDate, final MonetaryCurrency currency) { + + LoanRepaymentScheduleInstallment firstInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency) + .withDueDate(firstDueDate) + .withPrincipal("1000.00") + .withInterest("200.00") + .completed() + .build(); + + LoanRepaymentScheduleInstallment secondInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency) + .withDueDate(firstDueDate.plusMonths(1)) + .withPrincipal("1000.00") + .withInterest("200.00") + .build(); + + LoanRepaymentScheduleInstallment thirdInstallment = new LoanRepaymentScheduleInstallmentBuilder(currency) + .withDueDate(firstDueDate.plusMonths(2)) + .withPrincipal("1000.00") + .withInterest("200.00") + .build(); + + return Arrays.asList(firstInstallment, secondInstallment, thirdInstallment); + } + +} diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java index 40751b132..ab3c20ddf 100644 --- a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java @@ -5,17 +5,33 @@ import org.mifosng.platform.currency.domain.Money; public class LoanTransactionBuilder { - private Money amount = new MoneyBuilder().build(); - private LocalDate paymentDate = LocalDate.now(); + private Money transactionAmount = new MoneyBuilder().build(); + private LocalDate transactionDate = LocalDate.now(); + private boolean repayment = false; public LoanTransaction build() { - return LoanTransaction.repayment(amount, paymentDate); + + LoanTransaction transaction = null; + + if (repayment) { + transaction = LoanTransaction.repayment(transactionAmount, transactionDate); + } + + return transaction; } - public LoanTransactionBuilder with(Money newAmount) { - this.amount = newAmount; + public LoanTransactionBuilder with(final Money newAmount) { + this.transactionAmount = newAmount; return this; } + public LoanTransactionBuilder with(final LocalDate withTransactionDate) { + this.transactionDate = withTransactionDate; + return this; + } + public LoanTransactionBuilder repayment() { + this.repayment = true; + return this; + } }