Fixing myriad bugs in loan payment calculation.
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/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..20d2d37 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
@@ -35,9 +35,9 @@
 
 import javax.annotation.Nullable;
 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;
@@ -354,13 +354,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 +371,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 +408,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 +454,20 @@
     }
   }
 
-  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));
+  }
+
   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..23eb766 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,7 +20,6 @@
 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;
@@ -38,8 +37,6 @@
 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
  */
@@ -79,9 +76,16 @@
         precision);
 
     final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(precision));
-    final BigDecimal loanPaymentSize = loanPaymentInContextOfAccruedInterest(caseParameters.getMaximumBalance(), accrualRatesByPeriod.size(), geometricMeanAccrualRate);
+    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 +132,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,19 +178,17 @@
       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 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 BigDecimal loanPaymentInContextOfAccruedInterest(
           final BigDecimal initialBalance,
           final int periodCount,
@@ -208,7 +197,7 @@
       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();
+    return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact());
   }
 
   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/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index f672c6d..c5859c5 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,7 +263,15 @@
     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());
   }
@@ -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());
-  }
 }