create two types of repayment strategy from abstract template.

This commit is contained in:
Keith Woodlock
2012-07-28 20:56:46 +01:00
parent 58b3a2cc3e
commit dfd504cd73
7 changed files with 434 additions and 210 deletions
@@ -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<LoanTransaction> repaymentsOrWaivers,
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate,
final Money transactionAmount);
/**
* For early/'in advance' repayments.
*/
protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> 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);
}
@@ -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<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate, final Money transactionAmount) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* For early/'in advance' repayments, pays off principal component only.
*/
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
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
}
}
@@ -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<LoanTransaction> repaymentsOrWaivers,
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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;
}
}
@@ -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<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed);
}
@Override
protected boolean isTransactionInAdvanceOfInstallment(
final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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) {
}
}
@@ -132,6 +132,9 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@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<AppUser, Long> {
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 {
@@ -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;
}
}
@@ -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) {