Merge pull request #16 from myrlen/develop

fixing edge cases
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 1362783..0c6b96c 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -310,7 +310,7 @@
   AccountAssignment assignEntryToTeller() {
     final AccountAssignment entryAccountAssignment = new AccountAssignment();
     entryAccountAssignment.setDesignator(AccountDesignators.ENTRY);
-    entryAccountAssignment.setAccountIdentifier(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER);
+    entryAccountAssignment.setAccountIdentifier(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
     return entryAccountAssignment;
   }
 
diff --git a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
index 13ecedd..4665c8b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -60,7 +60,7 @@
   static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
   static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
   static final String DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER = "1313";
-  static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
+  static final String CUSTOMERS_DEPOSIT_ACCOUNT = "7352";
   static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
   static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
   static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
@@ -204,7 +204,7 @@
 
   private static Account tellerOneAccount() {
     final Account ret = new Account();
-    ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
+    ret.setIdentifier(CUSTOMERS_DEPOSIT_ACCOUNT);
     ret.setLedger(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
     return ret;
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 9ac056b..d4d2c27 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -336,6 +336,7 @@
 
   private BigDecimal findNextRepaymentAmount(
       final LocalDateTime forDateTime) {
+    AccountingFixture.mockBalance(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, BigDecimal.valueOf(2000_00L, 2));
     final Payment nextPayment = portfolioManager.getCostComponentsForAction(
         product.getIdentifier(),
         customerCase.getIdentifier(),
@@ -551,7 +552,7 @@
     debtors.add(new Debtor(AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, provisionForLosses.toPlainString()));
 
     final Set<Creditor> creditors = new HashSet<>();
-    creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toString()));
+    creditors.add(new Creditor(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, amount.toString()));
     creditors.add(new Creditor(AccountingFixture.PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, PROCESSING_FEE_AMOUNT.toPlainString()));
     creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
     creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
@@ -869,6 +870,8 @@
     logger.info("step7PaybackPartialAmount '{}' '{}'", amount, forDateTime);
     final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee.add(nonLateFees));
 
+    AccountingFixture.mockBalance(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, amount);
+
     final Payment payment = checkCostComponentForActionCorrect(
         product.getIdentifier(),
         customerCase.getIdentifier(),
@@ -906,7 +909,7 @@
     if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
       debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
     }
-    debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, tellerOneDebit.toPlainString()));
+    debtors.add(new Debtor(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, tellerOneDebit.toPlainString()));
 
     final Set<Creditor> creditors = new HashSet<>();
     creditors.add(new Creditor(customerLoanPrincipalIdentifier, principal.toPlainString()));
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
index ef3812d..7b06cec 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -154,7 +154,7 @@
 
     final RealRunningBalances balances = new RealRunningBalances(accountingAdapter, dataContextOfAction);
 
-    final BigDecimal currentBalance = balances.getAccountBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+    final BigDecimal currentBalance = balances.getAccountBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
     if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
       return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
 
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 4f37edd..9d1ff62 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
@@ -353,7 +353,7 @@
       customerCase.setCurrentState(Case.State.ACTIVE.name());
       caseRepository.save(customerCase);
     }
-    final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+    final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
 
     final BigDecimal newLoanPaymentSize = disbursePaymentBuilderService.getLoanPaymentSizeForSingleDisbursement(
         currentBalance.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY)),
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 9ae6402..29e994e 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
@@ -19,6 +19,7 @@
 import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
 import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
 import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.individuallending.internal.service.costcomponent.CostComponentService;
 import io.mifos.individuallending.internal.service.costcomponent.PaymentBuilder;
@@ -150,6 +151,8 @@
         requestedDisbursal = BigDecimal.ZERO;
       }
 
+      balances.adjustBalance(AccountDesignators.ENTRY, requestedRepayment);
+
       final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
       final PaymentBuilder paymentBuilder =
               CostComponentService.getCostComponentsForScheduledCharges(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
index d3bed9b..8c52c22 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
@@ -71,21 +71,25 @@
 
     if (requestedLoanPaymentSize != null) {
       loanPaymentSize = requestedLoanPaymentSize
-          .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+          .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO));
     }
     else if (scheduledAction.getActionPeriod() != null && scheduledAction.getActionPeriod().isLastPeriod()) {
-      loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+      loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
     }
     else {
       loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize()
-          .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+          .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO));
     }
 
+    final AvailableRunningBalancesWithLimits availableRunningBalanceWithLimits =
+        new AvailableRunningBalancesWithLimits(runningBalances);
+    availableRunningBalanceWithLimits.setUpperLimit(AccountDesignators.ENTRY, loanPaymentSize);
+
 
     return CostComponentService.getCostComponentsForScheduledCharges(
         scheduledChargesForThisAction,
         caseParameters.getBalanceRangeMaximum(),
-        runningBalances,
+        availableRunningBalanceWithLimits,
         dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
         BigDecimal.ZERO,
         loanPaymentSize,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java
new file mode 100644
index 0000000..3f8a072
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public class AvailableRunningBalancesWithLimits implements RunningBalances {
+  private final RunningBalances decoratedRunningBalances;
+
+  private final Map<String, BigDecimal> upperLimits = new HashMap<>();
+
+  AvailableRunningBalancesWithLimits(final RunningBalances decoratedRunningBalances) {
+    this.decoratedRunningBalances = decoratedRunningBalances;
+  }
+
+  void setUpperLimit(final String designator, final BigDecimal limit) {
+    upperLimits.put(designator, limit);
+  }
+
+  @Override
+  public BigDecimal getAvailableBalance(final String designator, final BigDecimal requestedAmount) {
+    final BigDecimal balance = getBalance(designator).orElse(requestedAmount);
+    final BigDecimal upperLimit = upperLimits.get(designator);
+    if (upperLimit == null)
+      return balance;
+    else
+      return upperLimit.min(balance);
+  }
+
+  @Override
+  public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
+    return decoratedRunningBalances.getAccountBalance(accountDesignator);
+  }
+
+  @Override
+  public BigDecimal getAccruedBalanceForCharge(final ChargeDefinition chargeDefinition) {
+    return decoratedRunningBalances.getAccruedBalanceForCharge(chargeDefinition);
+  }
+
+  @Override
+  public Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction) {
+    return decoratedRunningBalances.getStartOfTerm(dataContextOfAction);
+  }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
index 1b37c95..d759022 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
@@ -52,7 +52,7 @@
       final LocalDate forDate,
       final RunningBalances runningBalances)
   {
-    if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).compareTo(BigDecimal.ZERO) != 0)
+    if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO).compareTo(BigDecimal.ZERO) != 0)
       throw ServiceException.conflict("Cannot close loan until the balance is zero.");
 
 
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
index 6d7b0ca..91f90b7 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
@@ -131,11 +131,11 @@
       case MAXIMUM_BALANCE_DESIGNATOR:
         return maximumBalance;
       case RUNNING_BALANCE_DESIGNATOR: {
-        final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+        final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
         return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_GROUP));
       }
       case PRINCIPAL_DESIGNATOR: {
-        return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+        return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
       }
       case CONTRACTUAL_REPAYMENT_DESIGNATOR:
         return contractualRepayment;
@@ -144,10 +144,10 @@
       case REQUESTED_REPAYMENT_DESIGNATOR:
         return requestedRepayment.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY));
       case TO_ACCOUNT_DESIGNATOR:
-        return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator())
+        return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator()).orElse(BigDecimal.ZERO)
             .subtract(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getToAccountDesignator()));
       case FROM_ACCOUNT_DESIGNATOR:
-        return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator())
+        return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator()).orElse(BigDecimal.ZERO)
             .add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getFromAccountDesignator()));
       default:
         return BigDecimal.ZERO;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
index c843ee0..2a00590 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
@@ -55,7 +55,7 @@
       final LocalDate forDate,
       final RunningBalances runningBalances)
   {
-    final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+    final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
 
     if (requestedDisbursalSize != null &&
         dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
index 2821285..30b05be 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
@@ -37,7 +37,7 @@
   private final AccountingAdapter accountingAdapter;
   private final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper;
   private final DataContextOfAction dataContextOfAction;
-  private final ExpiringMap<String, BigDecimal> realAccountBalanceCache;
+  private final ExpiringMap<String, Optional<BigDecimal>> realAccountBalanceCache;
   @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
   private Optional<LocalDateTime> startOfTerm;
 
@@ -60,14 +60,14 @@
           else {
             accountIdentifier = Optional.of(designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator));
           }
-          return accountIdentifier.map(accountingAdapter::getCurrentAccountBalance).orElse(BigDecimal.ZERO);
+          return accountIdentifier.map(accountingAdapter::getCurrentAccountBalance);
         })
         .build();
     this.startOfTerm = Optional.empty();
   }
 
   @Override
-  public BigDecimal getAccountBalance(final String accountDesignator) {
+  public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
     return realAccountBalanceCache.get(accountDesignator);
   }
 
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
index 580293f..affd152 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
@@ -55,7 +55,7 @@
     //TODO: derive signs from IndividualLendingPatternFactory.individualLendingRequiredAccounts instead.
   }};
 
-  BigDecimal getAccountBalance(final String accountDesignator);
+  Optional<BigDecimal> getAccountBalance(final String accountDesignator);
 
   BigDecimal getAccruedBalanceForCharge(
       final ChargeDefinition chargeDefinition);
@@ -69,16 +69,26 @@
             dataContextOfAction.getCompoundIdentifer()));
   }
 
-  default BigDecimal getLedgerBalance(final String ledgerDesignator) {
+  default Optional<BigDecimal> getLedgerBalance(final String ledgerDesignator) {
     final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
     return individualLendingPattern.getAccountAssignmentsRequired().stream()
         .filter(requiredAccountAssignment -> ledgerDesignator.equals(requiredAccountAssignment.getGroup()))
         .map(RequiredAccountAssignment::getAccountDesignator)
         .map(this::getAccountBalance)
-        .reduce(BigDecimal.ZERO, BigDecimal::add);
+        .reduce(Optional.empty(), (x, y) -> {
+          if (x.isPresent() && y.isPresent())
+            return Optional.of(x.get().add(y.get()));
+          else if (x.isPresent())
+            return x;
+          else //noinspection OptionalIsPresent
+            if (y.isPresent())
+            return y;
+          else
+            return Optional.empty();
+        });
   }
 
-  default BigDecimal getBalance(final String designator) {
+  default Optional<BigDecimal> getBalance(final String designator) {
     final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
     if (individualLendingPattern.getAccountAssignmentGroups().contains(designator))
       return getLedgerBalance(designator);
@@ -86,28 +96,35 @@
       return getAccountBalance(designator);
   }
 
-  default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
-    if (accountDesignator.equals(AccountDesignators.ENTRY) ||
-        accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
-      return amount;
 
+  /**
+   *
+   * @param requestedAmount The requested amount is necessary as a parameter, because infinity is
+   *                        not available as a return value for BigDecimal.  There is no way to express that there is
+   *                        no limit, so when there is no limit, the requestedAmount is what is returned.
+   */
+  default BigDecimal getAvailableBalance(final String designator, final BigDecimal requestedAmount) {
+    return getBalance(designator).orElse(requestedAmount);
+  }
+
+  default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
     if (ACCOUNT_SIGNS.get(accountDesignator).signum() == -1)
       return amount;
     else
-      return amount.min(getBalance(accountDesignator));
+      return amount.min(getAvailableBalance(accountDesignator, amount));
   }
 
   default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
-    if (accountDesignator.equals(AccountDesignators.ENTRY) ||
-        accountDesignator.equals(AccountDesignators.EXPENSE) ||
-        accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
+    if (accountDesignator.equals(AccountDesignators.EXPENSE) ||
+        accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE) ||
+        accountDesignator.equals(AccountDesignators.GENERAL_LOSS_ALLOWANCE))
       return amount;
-    //entry account can achieve a "relative" negative balance, and
-    // product loss allowance can achieve an "absolute" negative balance.
+    //expense account can achieve a "relative" negative balance, and
+    // both loss allowance accounts can achieve an "absolute" negative balance.
 
     if (ACCOUNT_SIGNS.get(accountDesignator).signum() != -1)
       return amount;
     else
-      return amount.min(getBalance(accountDesignator));
+      return amount.min(getAvailableBalance(accountDesignator, amount));
   }
 }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
index fcd921d..cf7d47c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
@@ -42,8 +42,8 @@
   }
 
   @Override
-  public BigDecimal getAccountBalance(final String accountDesignator) {
-    return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
+  public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
+    return Optional.ofNullable(balances.get(accountDesignator));
   }
 
   @Override
@@ -58,7 +58,7 @@
     return Optional.ofNullable(startOfTerm);
   }
 
-  void adjustBalance(final String key, final BigDecimal amount) {
+  public void adjustBalance(final String key, final BigDecimal amount) {
     final BigDecimal sign = ACCOUNT_SIGNS.get(key);
     final BigDecimal currentValue = balances.getOrDefault(key, BigDecimal.ZERO);
     final BigDecimal newValue = currentValue.add(amount.multiply(sign));
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
index 8fbcda1..96add20 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
@@ -25,6 +25,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -40,16 +43,53 @@
   @Parameterized.Parameters
   public static Collection testCases() {
     final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
-    ret.add(simpleCase());
+    ret.add(new PaymentBuilderServiceTestCase("simple case"));
+    ret.add(disbursementFeesExceedFirstRepayment());
+    ret.add(lastLittleRepaymentZerosPrincipal());
+    ret.add(lastBigRepaymentZerosPrincipal());
+    ret.add(explicitlySetRepaymentSizeIsSmallerThanFees());
     return ret;
   }
 
-  private static PaymentBuilderServiceTestCase simpleCase() {
-    final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
-    testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
-    testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_INTEREST, testCase.accruedInterest.negate());
-    testCase.runningBalances.adjustBalance(AccountDesignators.INTEREST_ACCRUAL, testCase.accruedInterest);
-    return testCase;
+  private static PaymentBuilderServiceTestCase disbursementFeesExceedFirstRepayment() {
+    return new PaymentBuilderServiceTestCase("disbursement fees exceed first repayment")
+        .nonLateFees(BigDecimal.valueOf(200_00, 2))
+        .expectedPrincipalRepayment(BigDecimal.ZERO)
+        .expectedFeeRepayment(BigDecimal.valueOf(90_00, 2));
+  }
+
+  private static PaymentBuilderServiceTestCase lastLittleRepaymentZerosPrincipal() {
+    return new PaymentBuilderServiceTestCase("last repayment should zero principal, although standard repayment larger than principal")
+        .nonLateFees(BigDecimal.ZERO)
+        .endOfTerm(LocalDateTime.now(Clock.systemUTC()))
+        .forDate(LocalDateTime.now(Clock.systemUTC()))
+        .requestedPaymentSize(null)
+        .remainingPrincipal(BigDecimal.valueOf(42_00, 2))
+        .expectedPrincipalRepayment(BigDecimal.valueOf(42_00, 2))
+        .expectedInterestRepayment(BigDecimal.valueOf(10_00, 2))
+        .expectedFeeRepayment(BigDecimal.ZERO);
+  }
+
+  private static PaymentBuilderServiceTestCase lastBigRepaymentZerosPrincipal() {
+    return new PaymentBuilderServiceTestCase("last repayment should zero principal, although standard repayment smaller than principal")
+        .nonLateFees(BigDecimal.ZERO)
+        .endOfTerm(LocalDateTime.now(Clock.systemUTC()))
+        .forDate(LocalDateTime.now(Clock.systemUTC()))
+        .requestedPaymentSize(null)
+        .remainingPrincipal(BigDecimal.valueOf(142_00, 2))
+        .expectedPrincipalRepayment(BigDecimal.valueOf(142_00, 2))
+        .expectedInterestRepayment(BigDecimal.valueOf(10_00, 2))
+        .expectedFeeRepayment(BigDecimal.ZERO);
+  }
+
+  private static PaymentBuilderServiceTestCase explicitlySetRepaymentSizeIsSmallerThanFees() {
+    return new PaymentBuilderServiceTestCase("a payment size was chosen which is smaller than the fees due.")
+        .nonLateFees(BigDecimal.valueOf(50_00, 2))
+        .accruedInterest(BigDecimal.ZERO)
+        .requestedPaymentSize(BigDecimal.valueOf(49_00, 2))
+        .expectedInterestRepayment(BigDecimal.ZERO)
+        .expectedPrincipalRepayment(BigDecimal.ZERO)
+        .expectedFeeRepayment(BigDecimal.valueOf(49_00, 2));
   }
 
   private final PaymentBuilderServiceTestCase testCase;
@@ -63,13 +103,28 @@
     final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
         AcceptPaymentBuilderService::new, testCase);
 
-    final Payment payment = paymentBuilder.buildPayment(Action.ACCEPT_PAYMENT, Collections.emptySet(), testCase.forDate.toLocalDate());
-    Assert.assertNotNull(payment);
-    final Map<String, CostComponent> mappedCostComponents = payment.getCostComponents().stream()
-        .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
+    final Payment payment = paymentBuilder.buildPayment(
+        Action.ACCEPT_PAYMENT,
+        Collections.emptySet(),
+        testCase.forDate.toLocalDate());
 
-    Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.INTEREST_ID).getAmount());
-    Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.REPAY_INTEREST_ID).getAmount());
-    Assert.assertEquals(testCase.paymentSize.subtract(testCase.accruedInterest), mappedCostComponents.get(ChargeIdentifiers.REPAY_PRINCIPAL_ID).getAmount());
+    Assert.assertNotNull(payment);
+    final Map<String, BigDecimal> mappedCostComponents = payment.getCostComponents().stream()
+        .collect(Collectors.toMap(CostComponent::getChargeIdentifier, CostComponent::getAmount));
+
+    Assert.assertEquals(testCase.toString(),
+        testCase.expectedInterestRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.INTEREST_ID, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.toString(),
+        testCase.expectedInterestRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_INTEREST_ID, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.toString(),
+        testCase.expectedPrincipalRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_PRINCIPAL_ID, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.toString(),
+        testCase.expectedFeeRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_FEES_ID, BigDecimal.ZERO));
+
+    final BigDecimal expectedTotalRepaymentSize = testCase.expectedFeeRepayment.add(testCase.expectedInterestRepayment).add(testCase.expectedPrincipalRepayment);
+    Assert.assertEquals(expectedTotalRepaymentSize.negate(), payment.getBalanceAdjustments().getOrDefault(AccountDesignators.ENTRY, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.expectedFeeRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_FEES, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.expectedInterestRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_INTEREST, BigDecimal.ZERO));
+    Assert.assertEquals(testCase.expectedPrincipalRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, BigDecimal.ZERO));
   }
 }
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
index 80ba2a7..7d2152f 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
@@ -31,7 +31,6 @@
   @Test
   public void getPaymentBuilder() throws Exception {
     final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
-    testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
 
     final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
         ApplyInterestPaymentBuilderService::new, testCase);
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
index 4d3d907..246fb64 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
@@ -71,13 +71,13 @@
         .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
 
     Assert.assertEquals(
-        testCase.paymentSize,
+        testCase.configuredPaymentSize,
         mappedCostComponents.get(ChargeIdentifiers.DISBURSE_PAYMENT_ID).getAmount());
     Assert.assertEquals(
-        testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).setScale(2, BigDecimal.ROUND_HALF_EVEN),
+        testCase.configuredPaymentSize.multiply(BigDecimal.valueOf(1, 2)).setScale(2, BigDecimal.ROUND_HALF_EVEN),
         paymentBuilder.getBalanceAdjustments().get(AccountDesignators.PRODUCT_LOSS_ALLOWANCE));
     Assert.assertEquals(
-        testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).negate().setScale(2, BigDecimal.ROUND_HALF_EVEN),
+        testCase.configuredPaymentSize.multiply(BigDecimal.valueOf(1, 2)).negate().setScale(2, BigDecimal.ROUND_HALF_EVEN),
         paymentBuilder.getBalanceAdjustments().get(AccountDesignators.GENERAL_LOSS_ALLOWANCE));
   }
 
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
index 76bae20..29748f8 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
@@ -21,19 +21,24 @@
 
 class PaymentBuilderServiceTestCase {
   private final String description;
-  private LocalDateTime startOfTerm = LocalDateTime.of(2015, 1, 15, 0, 0);
+  LocalDateTime startOfTerm = LocalDateTime.of(2015, 1, 15, 0, 0);
   LocalDateTime endOfTerm = LocalDate.of(2015, 8, 15).atStartOfDay();
   LocalDateTime forDate = startOfTerm.plusMonths(1);
-  BigDecimal paymentSize = BigDecimal.valueOf(100_00, 2);
-  BigDecimal balance = BigDecimal.valueOf(2000_00, 2);
-  BigDecimal balanceRangeMaximum = BigDecimal.valueOf(1000_00, 2);
+  BigDecimal configuredPaymentSize = BigDecimal.valueOf(100_00, 2);
+  BigDecimal requestedPaymentSize = BigDecimal.valueOf(100_00, 2);
+  BigDecimal entryAccountBalance = BigDecimal.valueOf(10_000_00, 2);
+  BigDecimal remainingPrincipal = BigDecimal.valueOf(2000_00, 2);
+  BigDecimal balanceRangeMaximum = BigDecimal.valueOf(4000_00, 2);
   BigDecimal interestRate = BigDecimal.valueOf(5_00, 2);
   BigDecimal accruedInterest = BigDecimal.valueOf(10_00, 2);
-  SimulatedRunningBalances runningBalances;
+  BigDecimal nonLateFees = BigDecimal.valueOf(10_00, 2);
+  BigDecimal expectedPrincipalRepayment = BigDecimal.valueOf(80_00, 2);
+  BigDecimal expectedFeeRepayment = BigDecimal.valueOf(10_00, 2);
+  BigDecimal expectedInterestRepayment = BigDecimal.valueOf(10_00, 2);
+  BigDecimal generalLossAllowance = BigDecimal.valueOf(2000_00, 2);
 
   PaymentBuilderServiceTestCase(final String description) {
     this.description = description;
-    runningBalances = new SimulatedRunningBalances(startOfTerm);
   }
 
   PaymentBuilderServiceTestCase endOfTerm(LocalDateTime endOfTerm) {
@@ -46,23 +51,13 @@
     return this;
   }
 
-  PaymentBuilderServiceTestCase paymentSize(BigDecimal paymentSize) {
-    this.paymentSize = paymentSize;
+  PaymentBuilderServiceTestCase requestedPaymentSize(BigDecimal newVal) {
+    this.requestedPaymentSize = newVal;
     return this;
   }
 
-  PaymentBuilderServiceTestCase balance(BigDecimal balance) {
-    this.balance = balance;
-    return this;
-  }
-
-  PaymentBuilderServiceTestCase balanceRangeMaximum(BigDecimal balanceRangeMaximum) {
-    this.balanceRangeMaximum = balanceRangeMaximum;
-    return this;
-  }
-
-  PaymentBuilderServiceTestCase interestRate(BigDecimal interestRate) {
-    this.interestRate = interestRate;
+  PaymentBuilderServiceTestCase remainingPrincipal(BigDecimal newVal) {
+    this.remainingPrincipal = newVal;
     return this;
   }
 
@@ -71,8 +66,28 @@
     return this;
   }
 
-  PaymentBuilderServiceTestCase runningBalances(SimulatedRunningBalances newVal) {
-    this.runningBalances = newVal;
+  PaymentBuilderServiceTestCase nonLateFees(BigDecimal newVal) {
+    this.nonLateFees = newVal;
+    return this;
+  }
+
+  PaymentBuilderServiceTestCase expectedPrincipalRepayment(BigDecimal newVal) {
+    this.expectedPrincipalRepayment = newVal;
+    return this;
+  }
+
+  PaymentBuilderServiceTestCase expectedFeeRepayment(BigDecimal newVal) {
+    this.expectedFeeRepayment = newVal;
+    return this;
+  }
+
+  PaymentBuilderServiceTestCase expectedInterestRepayment(BigDecimal newVal) {
+    this.expectedInterestRepayment = newVal;
+    return this;
+  }
+
+  PaymentBuilderServiceTestCase generalLossAllowance(BigDecimal newVal) {
+    this.generalLossAllowance = newVal;
     return this;
   }
 
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
index 99a5c17..3496385 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
@@ -15,6 +15,7 @@
  */
 package io.mifos.individuallending.internal.service.costcomponent;
 
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
 import io.mifos.individuallending.internal.repository.CaseParametersEntity;
 import io.mifos.individuallending.internal.service.ChargeDefinitionService;
 import io.mifos.individuallending.internal.service.DataContextOfAction;
@@ -45,12 +46,21 @@
     customerCase.setEndOfTerm(testCase.endOfTerm);
     customerCase.setInterest(testCase.interestRate);
     final CaseParametersEntity caseParameters = new CaseParametersEntity();
-    caseParameters.setPaymentSize(testCase.paymentSize);
+    caseParameters.setPaymentSize(testCase.configuredPaymentSize);
     caseParameters.setBalanceRangeMaximum(testCase.balanceRangeMaximum);
     caseParameters.setPaymentCyclePeriod(1);
     caseParameters.setPaymentCycleTemporalUnit(ChronoUnit.MONTHS);
     caseParameters.setCreditWorthinessFactors(Collections.emptySet());
 
+    final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances(testCase.startOfTerm);
+    runningBalances.adjustBalance(AccountDesignators.ENTRY, testCase.entryAccountBalance);
+    runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.remainingPrincipal.negate());
+    runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_INTEREST, testCase.accruedInterest.negate());
+    runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_FEES, testCase.nonLateFees.negate());
+    runningBalances.adjustBalance(AccountDesignators.INTEREST_ACCRUAL, testCase.accruedInterest);
+
+    runningBalances.adjustBalance(AccountDesignators.GENERAL_LOSS_ALLOWANCE, testCase.generalLossAllowance.negate());
+
     final DataContextOfAction dataContextOfAction = new DataContextOfAction(
         product,
         customerCase,
@@ -58,8 +68,8 @@
         Collections.emptyList());
     return testSubject.getPaymentBuilder(
         dataContextOfAction,
-        testCase.paymentSize,
+        testCase.requestedPaymentSize,
         testCase.forDate.toLocalDate(),
-        testCase.runningBalances);
+        runningBalances);
   }
 }
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
index fe5f7d4..46e2dbc 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
@@ -15,7 +15,6 @@
  */
 package io.mifos.individuallending.internal.service.costcomponent;
 
-import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
 import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.individuallending.internal.service.DefaultChargeDefinitionsMocker;
@@ -26,6 +25,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -41,16 +41,14 @@
   @Parameterized.Parameters
   public static Collection testCases() {
     final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
-    ret.add(simpleCase());
-    //TODO: add use case for when the general loss allowance account doesn't have enough to cover the write off.
+    ret.add(new PaymentBuilderServiceTestCase("simple case"));
+    ret.add(lossProvisioningInsufficient());
     return ret;
   }
 
-  private static PaymentBuilderServiceTestCase simpleCase() {
-    final PaymentBuilderServiceTestCase ret = new PaymentBuilderServiceTestCase("simple case");
-    ret.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, ret.balance.negate());
-    ret.runningBalances.adjustBalance(AccountDesignators.GENERAL_LOSS_ALLOWANCE, ret.balance.negate());
-    return ret;
+  private static PaymentBuilderServiceTestCase lossProvisioningInsufficient() {
+    return new PaymentBuilderServiceTestCase("loss provisioning insufficient")
+        .generalLossAllowance(BigDecimal.ZERO);
   }
 
   private final PaymentBuilderServiceTestCase testCase;
@@ -70,8 +68,7 @@
         .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
 
     Assert.assertEquals(
-        testCase.balance,
+        testCase.remainingPrincipal,
         mappedCostComponents.get(ChargeIdentifiers.WRITE_OFF_ID).getAmount());
   }
-
 }