Merge pull request #38 from myrle-krantz/develop

continuing work on APPLY_PAYMENT command.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
index 65e8117..c0e0c56 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
@@ -23,29 +23,29 @@
 @SuppressWarnings("unused")
 public interface ChargeIdentifiers {
   String LOAN_FUNDS_ALLOCATION_NAME = "Loan funds allocation";
-  String LOAN_FUNDS_ALLOCATION_ID = nameToIdentifier(LOAN_FUNDS_ALLOCATION_NAME);
+  String LOAN_FUNDS_ALLOCATION_ID = "loan-funds-allocation";
   String RETURN_DISBURSEMENT_NAME = "Return disbursement";
-  String RETURN_DISBURSEMENT_ID = nameToIdentifier(RETURN_DISBURSEMENT_NAME);
+  String RETURN_DISBURSEMENT_ID = "return-disbursement";
   String INTEREST_NAME = "Interest";
-  String INTEREST_ID = nameToIdentifier(INTEREST_NAME);
+  String INTEREST_ID = "interest";
   String ALLOW_FOR_WRITE_OFF_NAME = "Allow for write-off";
-  String ALLOW_FOR_WRITE_OFF_ID = nameToIdentifier(ALLOW_FOR_WRITE_OFF_NAME);
+  String ALLOW_FOR_WRITE_OFF_ID = "allow-for-write-off";
   String LATE_FEE_NAME = "Late fee";
-  String LATE_FEE_ID = nameToIdentifier(LATE_FEE_NAME);
+  String LATE_FEE_ID = "late-fee";
   String DISBURSEMENT_FEE_NAME = "Disbursement fee";
-  String DISBURSEMENT_FEE_ID = nameToIdentifier(DISBURSEMENT_FEE_NAME);
+  String DISBURSEMENT_FEE_ID = "disbursement-fee";
   String DISBURSE_PAYMENT_NAME = "Disburse payment";
-  String DISBURSE_PAYMENT_ID = nameToIdentifier(DISBURSE_PAYMENT_NAME);
+  String DISBURSE_PAYMENT_ID = "disburse-payment";
   String TRACK_DISBURSAL_PAYMENT_NAME = "Track disburse payment";
-  String TRACK_DISBURSAL_PAYMENT_ID = nameToIdentifier(TRACK_DISBURSAL_PAYMENT_NAME);
+  String TRACK_DISBURSAL_PAYMENT_ID = "track-disburse-payment";
   String LOAN_ORIGINATION_FEE_NAME = "Loan origination fee";
-  String LOAN_ORIGINATION_FEE_ID = nameToIdentifier(LOAN_ORIGINATION_FEE_NAME);
+  String LOAN_ORIGINATION_FEE_ID = "loan-origination-fee";
   String PROCESSING_FEE_NAME = "Processing fee";
-  String PROCESSING_FEE_ID = nameToIdentifier(PROCESSING_FEE_NAME);
+  String PROCESSING_FEE_ID = "processing-fee";
   String REPAYMENT_NAME = "Repayment";
-  String REPAYMENT_ID = nameToIdentifier(REPAYMENT_NAME);
-  String TRACK_RETURN_PRINCIPAL_NAME = "Return principal";
-  String TRACK_RETURN_PRINCIPAL_ID = nameToIdentifier(TRACK_RETURN_PRINCIPAL_NAME);
+  String REPAYMENT_ID = "repayment";
+  String TRACK_RETURN_PRINCIPAL_NAME = "Track return principal";
+  String TRACK_RETURN_PRINCIPAL_ID = "track-return-principal";
   String MAXIMUM_BALANCE_DESIGNATOR = "{maximumbalance}";
   String RUNNING_BALANCE_DESIGNATOR = "{runningbalance}";
   String PRINCIPAL_ADJUSTMENT_DESIGNATOR = "{principaladjustment}";
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index 76eb969..cc28b11 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -17,6 +17,7 @@
 
 import javax.annotation.Nullable;
 import javax.validation.Valid;
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Objects;
 
@@ -30,7 +31,7 @@
 
   @Valid
   @Nullable
-  private List<CostComponent> costComponents;
+  private BigDecimal paymentSize;
 
   private String note;
   private String createdOn;
@@ -47,12 +48,13 @@
     this.oneTimeAccountAssignments = oneTimeAccountAssignments;
   }
 
-  public List<CostComponent> getCostComponents() {
-    return costComponents;
+  @Nullable
+  public BigDecimal getPaymentSize() {
+    return paymentSize;
   }
 
-  public void setCostComponents(List<CostComponent> costComponents) {
-    this.costComponents = costComponents;
+  public void setPaymentSize(@Nullable BigDecimal paymentSize) {
+    this.paymentSize = paymentSize;
   }
 
   public String getNote() {
@@ -85,20 +87,20 @@
     if (o == null || getClass() != o.getClass()) return false;
     Command command = (Command) o;
     return Objects.equals(oneTimeAccountAssignments, command.oneTimeAccountAssignments) &&
-        Objects.equals(costComponents, command.costComponents) &&
+        Objects.equals(paymentSize, command.paymentSize) &&
         Objects.equals(note, command.note);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(oneTimeAccountAssignments, costComponents, note);
+    return Objects.hash(oneTimeAccountAssignments, paymentSize, note);
   }
 
   @Override
   public String toString() {
     return "Command{" +
         "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
-        ", costComponents=" + costComponents +
+        ", paymentSize=" + paymentSize +
         ", note='" + note + '\'' +
         ", createdOn='" + createdOn + '\'' +
         ", createdBy='" + createdBy + '\'' +
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 437276d..2fd1429 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -195,8 +195,19 @@
                           final List<AccountAssignment> oneTimeAccountAssignments,
                           final String event,
                           final Case.State nextState) throws InterruptedException {
+    checkStateTransfer(productIdentifier, caseIdentifier, action, oneTimeAccountAssignments, BigDecimal.ZERO, event, nextState);
+  }
+
+  void checkStateTransfer(final String productIdentifier,
+                          final String caseIdentifier,
+                          final Action action,
+                          final List<AccountAssignment> oneTimeAccountAssignments,
+                          final BigDecimal paymentSize,
+                          final String event,
+                          final Case.State nextState) throws InterruptedException {
     final Command command = new Command();
     command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
+    command.setPaymentSize(paymentSize);
     portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
 
     Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier)));
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index 24f3441..472fc5d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -208,13 +208,13 @@
 
     final Set<Debtor> debtors = new HashSet<>();
     debtors.add(new Debtor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
-    debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER,
-        caseParameters.getMaximumBalance().subtract(DISBURSEMENT_FEE_AMOUNT).toPlainString()));
+    debtors.add(new Debtor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
+    debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
 
     final Set<Creditor> creditors = new HashSet<>();
     creditors.add(new Creditor(customerLoanAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
+    creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
     creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
-    creditors.add(new Creditor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
     AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
 
     expectedCurrentBalance = expectedCurrentBalance.add(caseParameters.getMaximumBalance());
@@ -268,6 +268,7 @@
         customerCase.getIdentifier(),
         Action.ACCEPT_PAYMENT,
         Collections.singletonList(assignEntryToTeller()),
+        expectedCurrentBalance,
         IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
         Case.State.ACTIVE); //Close has to be done explicitly.
     checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index eb7c4e0..6a30102 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -79,7 +79,7 @@
     individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
     individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
     individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
-    individualLendingRequiredAccounts.add(LOANS_PAYABLE);
+    individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
     individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
     individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
     individualLendingRequiredAccounts.add(DISBURSEMENT_FEE_INCOME);
@@ -132,8 +132,8 @@
     disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
     disbursePayment.setName(DISBURSE_PAYMENT_NAME);
     disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
-    disbursePayment.setFromAccountDesignator(ENTRY);
-    disbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
+    disbursePayment.setFromAccountDesignator(LOANS_PAYABLE);
+    disbursePayment.setToAccountDesignator(ENTRY);
     disbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
     disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     disbursePayment.setAmount(BigDecimal.ONE);
@@ -144,7 +144,7 @@
     trackPrincipalDisbursePayment.setName(TRACK_DISBURSAL_PAYMENT_NAME);
     trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
     trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
-    trackPrincipalDisbursePayment.setToAccountDesignator(LOANS_PAYABLE);
+    trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
     trackPrincipalDisbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
     trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     trackPrincipalDisbursePayment.setAmount(BigDecimal.ONE);
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index 41c3227..14ef0f9 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -102,7 +102,6 @@
         .map(entry -> mapCostComponentEntryToChargeInstance(
             Action.OPEN,
             entry,
-            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
             designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
@@ -142,7 +141,6 @@
         .map(entry -> mapCostComponentEntryToChargeInstance(
             Action.DENY,
             entry,
-            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
             designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
@@ -188,7 +186,6 @@
         .map(entry -> mapCostComponentEntryToChargeInstance(
             Action.APPROVE,
             entry,
-            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
             designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
@@ -229,7 +226,6 @@
               .map(entry -> mapCostComponentEntryToChargeInstance(
                   Action.DISBURSE,
                   entry,
-                  getRequestedChargeAmounts(command.getCommand().getCostComponents()),
                   designatorToAccountIdentifierMapper)),
           Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
         .collect(Collectors.toList());
@@ -278,7 +274,6 @@
         .map(entry -> mapCostComponentEntryToChargeInstance(
             Action.APPLY_INTEREST,
             entry,
-            Collections.emptyMap(),
             designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
@@ -309,16 +304,11 @@
           "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
 
     final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
-        costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction);
-
-    final Map<String, BigDecimal> requestedChargeAmounts
-        = getRequestedChargeAmounts(command.getCommand().getCostComponents());
+        costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction, command.getCommand().getPaymentSize());
 
     final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
         .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
-        .map(entry -> getChargeAmount(
-            requestedChargeAmounts.get(entry.getKey().getIdentifier()),
-            entry.getValue().getAmount()))
+        .map(entry -> entry.getValue().getAmount())
         .reduce(BigDecimal.ZERO, BigDecimal::add);
 
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
@@ -328,7 +318,6 @@
         .map(entry -> mapCostComponentEntryToChargeInstance(
             Action.ACCEPT_PAYMENT,
             entry,
-            requestedChargeAmounts,
             designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
@@ -398,38 +387,27 @@
   private static ChargeInstance mapCostComponentEntryToChargeInstance(
       final Action action,
       final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
-      final Map<String, BigDecimal> requestedChargeAmounts,
       final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
     final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
-
-    final BigDecimal requestedChargeAmount = requestedChargeAmounts.get(chargeDefinition.getIdentifier());
-    final BigDecimal configuredChargeAmount = costComponentEntry.getValue().getAmount();
-
-    final BigDecimal finalChargeAmount = getChargeAmount(requestedChargeAmount, configuredChargeAmount);
+    final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
 
     if (chargeDefinition.getAccrualAccountDesignator() != null) {
       if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
         return new ChargeInstance(
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
-            finalChargeAmount);
+            chargeAmount);
       else
         return new ChargeInstance(
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
-            finalChargeAmount);
+            chargeAmount);
     }
     else
       return new ChargeInstance(
           designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
           designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
-          finalChargeAmount);
-  }
-
-  private static BigDecimal getChargeAmount(
-      final BigDecimal requestedChargeAmount,
-      final BigDecimal configuredChargeAmount) {
-    return requestedChargeAmount != null ? requestedChargeAmount : configuredChargeAmount;
+          chargeAmount);
   }
 
   private static ChargeInstance getDisbursalChargeInstance(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index 439141a..5c871ba 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -30,14 +30,17 @@
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
 import io.mifos.portfolio.service.internal.repository.ProductRepository;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import org.javamoney.calc.common.Rate;
+import org.javamoney.moneta.Money;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Nullable;
+import javax.money.MonetaryAmount;
 import java.math.BigDecimal;
+import java.time.Clock;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.time.ZoneId;
 import java.util.*;
 import java.util.function.BiFunction;
 import java.util.stream.Collectors;
@@ -47,6 +50,7 @@
  */
 @Service
 public class CostComponentService {
+  private static final int EXTRA_PRECISION = 4;
   private static final int RUNNING_CALCULATION_PRECISION = 8;
 
   private final ProductRepository productRepository;
@@ -106,7 +110,7 @@
       case APPLY_INTEREST:
         return getCostComponentsForApplyInterest(dataContextOfAction);
       case ACCEPT_PAYMENT:
-        return getCostComponentsForAcceptPayment(dataContextOfAction);
+        return getCostComponentsForAcceptPayment(dataContextOfAction, null);
       case CLOSE:
         return getCostComponentsForClose(dataContextOfAction);
       case MARK_LATE:
@@ -210,7 +214,7 @@
         chargesSplitIntoScheduledAndAccrued.get(false),
         caseParameters.getMaximumBalance(),
         currentBalance,
-        BigDecimal.ZERO,//TODO: This needs to be provided by the user.
+        caseParameters.getMaximumBalance(),//TODO: This needs to be provided by the user.
         minorCurrencyUnitDigits);
   }
 
@@ -253,7 +257,8 @@
   }
 
   public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
-      final DataContextOfAction dataContextOfAction)
+      final DataContextOfAction dataContextOfAction,
+      final @Nullable BigDecimal requestedLoanPaymentSize)
   {
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -272,11 +277,24 @@
         caseParameters
     );
 
-    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+    final BigDecimal loanPaymentSize;
+    if (requestedLoanPaymentSize != null)
+      loanPaymentSize = requestedLoanPaymentSize;
+    else {
+      final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
+          today(),
+          caseParameters);
+      final List<ScheduledCharge> hypotheticalScheduledCharges = individualLoanService.getScheduledCharges(
+          productIdentifier,
+          hypotheticalScheduledActions);
+      loanPaymentSize = getLoanPaymentSize(currentBalance, minorCurrencyUnitDigits, hypotheticalScheduledCharges);
+    }
+
+    final List<ScheduledCharge> scheduledChargesForThisAction = individualLoanService.getScheduledCharges(
         productIdentifier,
         Collections.singletonList(scheduledAction));
 
-    final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+    final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
         .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.ACCEPT_PAYMENT)));
 
     final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
@@ -285,12 +303,14 @@
         .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
             chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
 
+
+
     return getCostComponentsForScheduledCharges(
         accruedCostComponents,
         chargesSplitIntoScheduledAndAccrued.get(false),
         caseParameters.getMaximumBalance(),
         currentBalance,
-        BigDecimal.ZERO,//TODO: This needs to be provided by the user, or calculated.  ZERO is wrong.
+        loanPaymentSize,
         minorCurrencyUnitDigits);
   }
 
@@ -354,13 +374,14 @@
       final BigDecimal loanPaymentSize,
       final int minorCurrencyUnitDigits) {
     BigDecimal balanceAdjustment = BigDecimal.ZERO;
+    BigDecimal currentRunningBalance = runningBalance;
 
     final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
 
     for (Map.Entry<ChargeDefinition, CostComponent> entry : accruedCostComponents.entrySet()) {
       costComponentMap.put(entry.getKey(), entry.getValue());
 
-      if (chargeDefinitionTouchesCustomerLoanAccount(entry.getKey()))
+      if (chargeDefinitionTouchesAccount(entry.getKey(), AccountDesignators.CUSTOMER_LOAN))
         balanceAdjustment = balanceAdjustment.add(entry.getValue().getAmount());
     }
 
@@ -370,35 +391,35 @@
     for (final ScheduledCharge scheduledCharge : partitionedCharges.get(false))
     {
       final CostComponent costComponent = costComponentMap
-          .computeIfAbsent(scheduledCharge.getChargeDefinition(),
-              chargeIdentifier -> constructEmptyCostComponent(scheduledCharge));
+          .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
 
       final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge)
-          .apply(maximumBalance, runningBalance)
+          .apply(maximumBalance, currentRunningBalance)
           .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
-      if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
+      if (chargeDefinitionTouchesAccount(scheduledCharge.getChargeDefinition(), AccountDesignators.CUSTOMER_LOAN))
         balanceAdjustment = balanceAdjustment.add(chargeAmount);
       costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+      currentRunningBalance = currentRunningBalance.add(chargeAmount);
     }
 
     final BigDecimal principalAdjustment = loanPaymentSize.subtract(balanceAdjustment);
     for (final ScheduledCharge scheduledCharge : partitionedCharges.get(true))
     {
       final CostComponent costComponent = costComponentMap
-          .computeIfAbsent(scheduledCharge.getChargeDefinition(),
-              chargeIdentifier -> constructEmptyCostComponent(scheduledCharge));
+          .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
 
       final BigDecimal chargeAmount = applyPrincipalAdjustmentCharge(scheduledCharge, principalAdjustment)
           .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
-      if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
+      if (chargeDefinitionTouchesAccount(scheduledCharge.getChargeDefinition(), AccountDesignators.CUSTOMER_LOAN))
         balanceAdjustment = balanceAdjustment.add(chargeAmount);
       costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+      currentRunningBalance = currentRunningBalance.add(chargeAmount);
     }
 
     return new CostComponentsForRepaymentPeriod(
         runningBalance,
         costComponentMap,
-        balanceAdjustment);
+        balanceAdjustment.negate());
   }
 
   private static BigDecimal applyPrincipalAdjustmentCharge(
@@ -407,9 +428,9 @@
     return scheduledCharge.getChargeDefinition().getAmount().multiply(principalAdjustment);
   }
 
-  private static CostComponent constructEmptyCostComponent(ScheduledCharge scheduledCharge) {
+  private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
     final CostComponent ret = new CostComponent();
-    ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
+    ret.setChargeIdentifier(chargeDefinition.getIdentifier());
     ret.setAmount(BigDecimal.ZERO);
     return ret;
   }
@@ -453,13 +474,41 @@
     }
   }
 
-  private static boolean chargeDefinitionTouchesCustomerLoanAccount(final ChargeDefinition chargeDefinition)
+  private static boolean chargeDefinitionTouchesCustomerVisibleAccount(final ChargeDefinition chargeDefinition)
   {
-    return chargeDefinition.getToAccountDesignator().equals(AccountDesignators.CUSTOMER_LOAN) ||
-        chargeDefinition.getFromAccountDesignator().equals(AccountDesignators.CUSTOMER_LOAN) ||
-        (chargeDefinition.getAccrualAccountDesignator() != null && chargeDefinition.getAccrualAccountDesignator().equals(AccountDesignators.CUSTOMER_LOAN));
+    return chargeDefinitionTouchesAccount(chargeDefinition, AccountDesignators.CUSTOMER_LOAN) ||
+        chargeDefinitionTouchesAccount(chargeDefinition, AccountDesignators.ENTRY);
   }
+
+  private static boolean chargeDefinitionTouchesAccount(final ChargeDefinition chargeDefinition, final String accountDesignator)
+  {
+    return chargeDefinition.getToAccountDesignator().equals(accountDesignator) ||
+        chargeDefinition.getFromAccountDesignator().equals(accountDesignator) ||
+        (chargeDefinition.getAccrualAccountDesignator() != null && chargeDefinition.getAccrualAccountDesignator().equals(accountDesignator));
+  }
+
+  static BigDecimal getLoanPaymentSize(final BigDecimal startingBalance,
+                                       final int minorCurrencyUnitDigits,
+                                       final List<ScheduledCharge> scheduledCharges) {
+    final int precision = startingBalance.precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
+    final Map<Period, BigDecimal> accrualRatesByPeriod
+        = PeriodChargeCalculator.getPeriodAccrualRates(scheduledCharges, precision);
+
+    final int periodCount = accrualRatesByPeriod.size();
+    if (periodCount == 0)
+      return startingBalance;
+
+    final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream()
+        .collect(RateCollectors.geometricMean(precision));
+
+    final MonetaryAmount presentValue = AnnuityPayment.calculate(
+        Money.of(startingBalance, "XXX"),
+        Rate.of(geometricMeanAccrualRate),
+        periodCount);
+    return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact());
+  }
+
   private static LocalDate today() {
-    return LocalDate.now(ZoneId.of("UTC"));
+    return LocalDate.now(Clock.systemUTC());
   }
 }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
index b7a09e2..ebdf4cf 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
@@ -20,17 +20,13 @@
 import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
 import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
 import io.mifos.portfolio.api.v1.domain.Product;
 import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
 import io.mifos.portfolio.service.internal.service.ProductService;
-import org.javamoney.calc.common.Rate;
-import org.javamoney.moneta.Money;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Nonnull;
-import javax.money.MonetaryAmount;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.*;
@@ -38,25 +34,19 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.REPAYMENT_ID;
-
 /**
  * @author Myrle Krantz
  */
 @Service
 public class IndividualLoanService {
-  private static final int EXTRA_PRECISION = 4;
   private final ProductService productService;
   private final ChargeDefinitionService chargeDefinitionService;
-  private final PeriodChargeCalculator periodChargeCalculator;
 
   @Autowired
   public IndividualLoanService(final ProductService productService,
-                               final ChargeDefinitionService chargeDefinitionService,
-                               final PeriodChargeCalculator periodChargeCalculator) {
+                               final ChargeDefinitionService chargeDefinitionService) {
     this.productService = productService;
     this.chargeDefinitionService = chargeDefinitionService;
-    this.periodChargeCalculator = periodChargeCalculator;
   }
 
   public PlannedPaymentPage getPlannedPaymentsPage(
@@ -73,15 +63,16 @@
 
     final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, scheduledActions);
 
-    final int precision = caseParameters.getMaximumBalance().precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
-    final Map<Period, BigDecimal> accrualRatesByPeriod
-        = periodChargeCalculator.getPeriodAccrualRates(scheduledCharges,
-        precision);
+    final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
+        caseParameters.getMaximumBalance(),
+        minorCurrencyUnitDigits,
+        scheduledCharges);
 
-    final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(precision));
-    final BigDecimal loanPaymentSize = loanPaymentInContextOfAccruedInterest(caseParameters.getMaximumBalance(), accrualRatesByPeriod.size(), geometricMeanAccrualRate);
-
-    final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(caseParameters.getMaximumBalance(), minorCurrencyUnitDigits, scheduledCharges, loanPaymentSize);
+    final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(
+        caseParameters.getMaximumBalance(),
+        minorCurrencyUnitDigits,
+        scheduledCharges,
+        loanPaymentSize);
 
     final Set<ChargeName> chargeNames = scheduledCharges.stream()
             .map(IndividualLoanService::chargeNameFromChargeDefinition)
@@ -128,56 +119,43 @@
             chargeDefinitionsMappedByAccrueAction);
   }
 
-  private static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
-  {
-    @Override
-    public int compare(ScheduledCharge o1, ScheduledCharge o2) {
-      int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
-      if (ret == 0)
-        ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
-      if (ret == 0)
-        return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
-      else
-        return ret;
-    }
-  }
-
   static private List<PlannedPayment> getPlannedPaymentsElements(
       final BigDecimal initialBalance,
       final int minorCurrencyUnitDigits,
       final List<ScheduledCharge> scheduledCharges,
       final BigDecimal loanPaymentSize) {
-    final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
+    final Map<Period, Set<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
             = scheduledCharges.stream()
-            .collect(Collectors.groupingBy(scheduledCharge -> {
-                  final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
-                  if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
-                    return new Period(null, null);
-                  else
-                    return scheduledAction.repaymentPeriod;
-                  },
-                    Collectors.mapping(x -> x,
-                            Collector.of(
-                                    () -> new TreeSet<>(new ScheduledChargeComparator()),
-                                    SortedSet::add,
-                                    (left, right) -> { left.addAll(right); return left; }))));
+            .collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
+                    Collectors.mapping(x -> x, Collectors.toSet())));
 
-    final SortedSet<Period> sortedRepaymentPeriods
-            = orderedScheduledChargesGroupedByPeriod.keySet().stream()
-            .collect(Collector.of(TreeSet::new, TreeSet::add, (left, right) -> { left.addAll(right); return left; }));
+    final List<Period> sortedRepaymentPeriods
+        = orderedScheduledChargesGroupedByPeriod.keySet().stream()
+        .sorted()
+        .collect(Collector.of(ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }));
 
     BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
     final List<PlannedPayment> plannedPayments = new ArrayList<>();
     for (final Period repaymentPeriod : sortedRepaymentPeriods)
     {
-      final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
+      final BigDecimal currentLoanPaymentSize;
+      if (repaymentPeriod.isDefined()) {
+        if (balance.compareTo(loanPaymentSize) < 0)
+          currentLoanPaymentSize = balance;
+        else
+          currentLoanPaymentSize = loanPaymentSize;
+      }
+      else
+        currentLoanPaymentSize = BigDecimal.ZERO;
+
+      final Set<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
       final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
               CostComponentService.getCostComponentsForScheduledCharges(
                   Collections.emptyMap(),
                   scheduledChargesInPeriod,
                   balance,
                   balance,
-                  loanPaymentSize,
+                  currentLoanPaymentSize,
                   minorCurrencyUnitDigits);
 
       final PlannedPayment plannedPayment = new PlannedPayment();
@@ -187,28 +165,15 @@
       plannedPayment.setRemainingPrincipal(balance);
       plannedPayments.add(plannedPayment);
     }
-    if (balance.compareTo(BigDecimal.ZERO) != 0)
-    {
-      final PlannedPayment lastPayment = plannedPayments.get(plannedPayments.size() - 1);
-      final Optional<CostComponent> lastPaymentPayment = lastPayment.getCostComponents().stream()
-              .filter(x -> x.getChargeIdentifier().equals(REPAYMENT_ID)).findAny();
-      lastPaymentPayment.ifPresent(x -> {
-        x.setAmount(x.getAmount().subtract(lastPayment.getRemainingPrincipal()));
-        lastPayment.setRemainingPrincipal(BigDecimal.ZERO.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN));
-      });
-    }
     return plannedPayments;
   }
 
-  private BigDecimal loanPaymentInContextOfAccruedInterest(
-          final BigDecimal initialBalance,
-          final int periodCount,
-          final BigDecimal geometricMeanOfInterest) {
-    if (periodCount == 0)
-      throw new IllegalStateException("To calculate a loan payment there must be at least one payment period.");
-
-    final MonetaryAmount presentValue = AnnuityPayment.calculate(Money.of(initialBalance, "XXX"), Rate.of(geometricMeanOfInterest), periodCount);
-    return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).negate();
+  private static Period getPeriodFromScheduledCharge(final ScheduledCharge scheduledCharge) {
+    final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
+    if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
+      return new Period(null, null);
+    else
+      return scheduledAction.repaymentPeriod;
   }
 
   private List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
index 7718553..2b28eb5 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
@@ -66,6 +66,10 @@
     return this.getBeginDate().compareTo(date) <= 0 && this.getEndDate().compareTo(date) > 0;
   }
 
+  boolean isDefined() {
+    return beginDate != null || endDate != null;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index d6186a7..f43aa99 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -34,7 +34,7 @@
   {
   }
 
-  Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
+  static Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
     return scheduledCharges.stream()
             .filter(PeriodChargeCalculator::accruedCharge)
             .collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index f672c6d..209dafd 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
@@ -46,7 +46,6 @@
  */
 @RunWith(Parameterized.class)
 public class IndividualLoanServiceTest {
-
   private static class ActionDatePair {
     final Action action;
     final LocalDate localDate;
@@ -86,8 +85,19 @@
     private int minorCurrencyUnitDigits = 2;
     private CaseParameters caseParameters;
     private LocalDate initialDisbursementDate;
-    private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
-    private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.REPAYMENT_ID));
+    private List<ChargeDefinition> chargeDefinitions;
+    private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(
+        PROCESSING_FEE_ID,
+        LOAN_FUNDS_ALLOCATION_ID,
+        RETURN_DISBURSEMENT_ID,
+        LOAN_ORIGINATION_FEE_ID,
+        INTEREST_ID,
+        DISBURSEMENT_FEE_ID,
+        REPAYMENT_ID,
+        TRACK_DISBURSAL_PAYMENT_ID,
+        TRACK_RETURN_PRINCIPAL_ID,
+        DISBURSE_PAYMENT_ID
+        ));
     private Map<ActionDatePair, List<ChargeDefinition>> chargeDefinitionsForActions = new HashMap<>();
     //This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
     //account designators.  Don't copy the code around charge instances in this test without thinking about what you're
@@ -112,18 +122,8 @@
       return this;
     }
 
-    TestCase chargeDefinitionsMappedByAction(final Map<String, List<ChargeDefinition>> newVal) {
-      this.chargeDefinitionsMappedByAction = newVal;
-      return this;
-    }
-
-    TestCase expectedChargeIdentifiers(final Set<String> newVal) {
-      this.expectedChargeIdentifiers = newVal;
-      return this;
-    }
-
-    TestCase expectAdditionalChargeIdentifier(final String newVal) {
-      this.expectedChargeIdentifiers.add(newVal);
+    TestCase chargeDefinitions(final List<ChargeDefinition> newVal) {
+      this.chargeDefinitions = newVal;
       return this;
     }
 
@@ -154,6 +154,8 @@
   private final TestCase testCase;
   private final IndividualLoanService testSubject;
   private final Product product;
+  private final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction;
+  private final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction;
 
 
   private static TestCase simpleCase()
@@ -164,31 +166,28 @@
     caseParameters.setTermRange(new TermRange(ChronoUnit.WEEKS, 3));
     caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 0, null, null));
 
-    //I know: this is cheating in a unit test.  But I really didn't want to put this data together by hand.
-    final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.01);
     final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
-    chargeDefinitionsMappedByAction.put(Action.OPEN.name(), Collections.singletonList(processingFeeCharge));
     final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
-    final List<ChargeDefinition> existingApprovalCharges = chargeDefinitionsMappedByAction.get(Action.APPROVE.name());
-    final List<ChargeDefinition> approvalChargesWithLoanOriginationFeeReplaced = existingApprovalCharges.stream().map(x -> {
-      if (x.getIdentifier().equals(LOAN_ORIGINATION_FEE_ID))
-        return loanOriginationFeeCharge;
-      else
-        return x;
+    final List<ChargeDefinition> defaultChargesWithFeesReplaced =
+    chargesWithInterestRate(0.01).stream().map(x -> {
+      switch (x.getIdentifier()) {
+        case PROCESSING_FEE_ID:
+          return processingFeeCharge;
+        case LOAN_ORIGINATION_FEE_ID:
+          return loanOriginationFeeCharge;
+        default:
+          return x;
+      }
     }).collect(Collectors.toList());
-    chargeDefinitionsMappedByAction.put(Action.APPROVE.name(), approvalChargesWithLoanOriginationFeeReplaced);
 
     return new TestCase("simpleCase")
-            .minorCurrencyUnitDigits(2)
-            .caseParameters(caseParameters)
-            .initialDisbursementDate(initialDisbursementDate)
-            .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
-            .expectAdditionalChargeIdentifier(PROCESSING_FEE_ID)
-            .expectAdditionalChargeIdentifier(LOAN_FUNDS_ALLOCATION_ID)
-            .expectAdditionalChargeIdentifier(LOAN_ORIGINATION_FEE_ID)
-            .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
-            .expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
-                Collections.singletonList(loanOriginationFeeCharge));
+        .minorCurrencyUnitDigits(2)
+        .caseParameters(caseParameters)
+        .initialDisbursementDate(initialDisbursementDate)
+        .chargeDefinitions(defaultChargesWithFeesReplaced)
+        .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
+        .expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
+            Collections.singletonList(loanOriginationFeeCharge));
   }
 
   private static TestCase yearLoanTestCase()
@@ -200,13 +199,13 @@
     caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 0, null, null));
     caseParameters.setMaximumBalance(BigDecimal.valueOf(200000));
 
-    final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.10);
+    final List<ChargeDefinition> charges = chargesWithInterestRate(0.10);
 
     return new TestCase("yearLoanTestCase")
-            .minorCurrencyUnitDigits(3)
-            .caseParameters(caseParameters)
-            .initialDisbursementDate(initialDisbursementDate)
-            .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction);
+        .minorCurrencyUnitDigits(3)
+        .caseParameters(caseParameters)
+        .initialDisbursementDate(initialDisbursementDate)
+        .chargeDefinitions(charges);
   }
 
   private static TestCase chargeDefaultsCase()
@@ -217,40 +216,24 @@
     caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 1, 0, 0));
     caseParameters.setMaximumBalance(BigDecimal.valueOf(2000));
 
-    final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.05);
+    final List<ChargeDefinition> charges = chargesWithInterestRate(0.05);
 
     return new TestCase("chargeDefaultsCase")
-            .minorCurrencyUnitDigits(2)
-            .caseParameters(caseParameters)
-            .initialDisbursementDate(initialDisbursementDate)
-            .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
-            .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, LOAN_FUNDS_ALLOCATION_ID, RETURN_DISBURSEMENT_ID, LOAN_ORIGINATION_FEE_ID, INTEREST_ID, DISBURSEMENT_FEE_ID, REPAYMENT_ID)));
+        .minorCurrencyUnitDigits(2)
+        .caseParameters(caseParameters)
+        .initialDisbursementDate(initialDisbursementDate)
+        .chargeDefinitions(charges);
   }
 
-  private static Map<String, List<ChargeDefinition>> constructCharges(final double interestRate) {
+  private static List<ChargeDefinition> chargesWithInterestRate(final double interestRate) {
     final List<ChargeDefinition> defaultLoanCharges = IndividualLendingPatternFactory.defaultIndividualLoanCharges();
 
-    final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = defaultLoanCharges.stream()
-            .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
-                    Collectors.mapping(x -> x, Collectors.toList())));
+    defaultLoanCharges.forEach(x -> {
+      if (x.getIdentifier().equals(ChargeIdentifiers.INTEREST_ID))
+        x.setAmount(BigDecimal.valueOf(interestRate));
+    });
 
-    chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(interestRate, ChronoUnit.YEARS));
-    return chargeDefinitionsMappedByAction;
-  }
-
-  private static List<ChargeDefinition> getInterestChargeDefinition(final double amount, final ChronoUnit forCycleSizeUnit) {
-    final ChargeDefinition ret = new ChargeDefinition();
-    ret.setAmount(BigDecimal.valueOf(amount));
-    ret.setIdentifier(ChargeIdentifiers.INTEREST_ID);
-    ret.setAccrueAction(Action.APPLY_INTEREST.name());
-    ret.setChargeAction(Action.ACCEPT_PAYMENT.name());
-    ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
-    ret.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
-    ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
-    ret.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
-    ret.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
-    ret.setForCycleSizeUnit(forCycleSizeUnit);
-    return Collections.singletonList(ret);
+    return defaultLoanCharges;
   }
 
   private static ChargeDefinition getFixedSingleChargeDefinition(
@@ -271,25 +254,6 @@
     return ret;
   }
 
-  private static ChargeDefinition getProportionalSingleChargeDefinition(
-          final double amount,
-          final Action action,
-          final String chargeIdentifier,
-          final String fromAccountDesignator,
-          final String toAccountDesignator) {
-    final ChargeDefinition ret = new ChargeDefinition();
-    ret.setAmount(BigDecimal.valueOf(amount));
-    ret.setIdentifier(chargeIdentifier);
-    ret.setAccrueAction(null);
-    ret.setChargeAction(action.name());
-    ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
-    ret.setProportionalTo(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR);
-    ret.setFromAccountDesignator(fromAccountDesignator);
-    ret.setToAccountDesignator(toAccountDesignator);
-    ret.setForCycleSizeUnit(null);
-    return ret;
-  }
-
   public IndividualLoanServiceTest(final TestCase testCase)
   {
     this.testCase = testCase;
@@ -299,9 +263,17 @@
     product = new Product();
     product.setMinorCurrencyUnitDigits(testCase.minorCurrencyUnitDigits);
     Mockito.doReturn(Optional.of(product)).when(productServiceMock).findByIdentifier(testCase.productIdentifier);
-    Mockito.doReturn(testCase.chargeDefinitionsMappedByAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
+    chargeDefinitionsByChargeAction = testCase.chargeDefinitions.stream()
+        .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
+            Collectors.mapping(x -> x, Collectors.toList())));
+    chargeDefinitionsByAccrueAction = testCase.chargeDefinitions.stream()
+        .filter(x -> x.getAccrueAction() != null)
+        .collect(Collectors.groupingBy(ChargeDefinition::getAccrueAction,
+            Collectors.mapping(x -> x, Collectors.toList())));
+    Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
+    Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
 
-    testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new PeriodChargeCalculator());
+    testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock);
   }
 
   @Test
@@ -320,32 +292,34 @@
             .collect(Collectors.toList());
 
     //Remaining principal should correspond with the other cost components.
-    Stream.iterate(0, x -> x+1).limit(allPlannedPayments.size()-2).forEach(x ->
-            {
-              final BigDecimal costComponentSum = allPlannedPayments.get(x+1).getCostComponents().stream()
-                      .map(CostComponent::getAmount)
-                      .reduce(BigDecimal::add)
-                      .orElse(BigDecimal.ZERO)
-                      .negate();
-              final BigDecimal principalDifference = allPlannedPayments.get(x).getRemainingPrincipal().subtract(allPlannedPayments.get(x + 1).getRemainingPrincipal());
-              Assert.assertEquals(costComponentSum, principalDifference);
-              Assert.assertNotEquals("Remaining principle should always be positive or zero.",
-                      allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
-            }
-    );
+    final Set<BigDecimal> customerRepayments = Stream.iterate(1, x -> x + 1).limit(allPlannedPayments.size() - 1).map(x ->
+        {
+          final BigDecimal costComponentSum = allPlannedPayments.get(x).getCostComponents().stream()
+              .filter(this::includeCostComponentsInSumCheck)
+              .map(CostComponent::getAmount)
+              .reduce(BigDecimal::add)
+              .orElse(BigDecimal.ZERO);
+          final BigDecimal principalDifference = allPlannedPayments.get(x-1).getRemainingPrincipal().subtract(allPlannedPayments.get(x).getRemainingPrincipal());
+          Assert.assertEquals(costComponentSum, principalDifference);
+          Assert.assertNotEquals("Remaining principle should always be positive or zero.",
+              allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
+          return costComponentSum;
+        }
+    ).collect(Collectors.toSet());
 
     //All entries should have the correct scale.
     allPlannedPayments.forEach(x -> {
       x.getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
       Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getRemainingPrincipal().scale());
+      final int uniqueChargeIdentifierCount = x.getCostComponents().stream()
+          .map(CostComponent::getChargeIdentifier)
+          .collect(Collectors.toSet())
+          .size();
+      Assert.assertEquals("There should be only one cost component per charge per planned payment.",
+          x.getCostComponents().size(), uniqueChargeIdentifierCount);
     });
 
     //All customer payments should be within one percent of each other.
-    final Set<BigDecimal> customerRepayments = allPlannedPayments.stream()
-        .map(this::getCustomerRepayment)
-        .filter(Optional::isPresent)
-        .map(Optional::get)
-        .collect(Collectors.toSet());
     final Optional<BigDecimal> maxPayment = customerRepayments.stream().max(BigDecimal::compareTo);
     final Optional<BigDecimal> minPayment = customerRepayments.stream().min(BigDecimal::compareTo);
     Assert.assertTrue(maxPayment.isPresent());
@@ -357,7 +331,7 @@
     Assert.assertEquals(BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
             allPlannedPayments.get(allPlannedPayments.size()-1).getRemainingPrincipal());
 
-    //All charge identifers should be associated with a name on the returned page.
+    //All charge identifiers should be associated with a name on the returned page.
     final Set<String> resultChargeIdentifiers = firstPage.getChargeNames().stream()
             .map(ChargeName::getIdentifier)
             .collect(Collectors.toSet());
@@ -365,6 +339,22 @@
     Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
   }
 
+  private boolean includeCostComponentsInSumCheck(CostComponent costComponent) {
+    switch (costComponent.getChargeIdentifier()) {
+      case ChargeIdentifiers.INTEREST_ID:
+      case ChargeIdentifiers.DISBURSEMENT_FEE_ID:
+      case ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID:
+      case ChargeIdentifiers.LATE_FEE_ID:
+      case ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID:
+      case ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID:
+      case ChargeIdentifiers.PROCESSING_FEE_ID:
+        return true;
+      default:
+        return false;
+
+    }
+  }
+
   @Test
   public void getScheduledCharges() {
     final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
@@ -389,7 +379,7 @@
         .collect(Collectors.toList());
     final long expectedAcceptPayments = scheduledActions.stream()
         .filter(x -> x.action == Action.ACCEPT_PAYMENT).count();
-    final List<ChargeDefinition> chargeDefinitionsMappedToAcceptPayment = testCase.chargeDefinitionsMappedByAction.get(Action.ACCEPT_PAYMENT.name());
+    final List<ChargeDefinition> chargeDefinitionsMappedToAcceptPayment = chargeDefinitionsByChargeAction.get(Action.ACCEPT_PAYMENT.name());
     final int numberOfChangeDefinitionsMappedToAcceptPayment = chargeDefinitionsMappedToAcceptPayment == null ? 0 : chargeDefinitionsMappedToAcceptPayment.size();
     Assert.assertEquals("check for correct number of scheduled charges for accept payment",
         expectedAcceptPayments*numberOfChangeDefinitionsMappedToAcceptPayment,
@@ -409,12 +399,4 @@
     final BigDecimal percentDifference = difference.divide(maxPayment, 4, BigDecimal.ROUND_UP);
     return percentDifference.doubleValue();
   }
-
-  private Optional<BigDecimal> getCustomerRepayment(final PlannedPayment plannedPayment) {
-    final Optional<CostComponent> ret = plannedPayment.getCostComponents().stream()
-            .filter(y -> y.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
-            .findAny();
-
-    return ret.map(x -> x.getAmount().abs());
-  }
 }