Fixing bug resulting from the implementation of JournalEntry credits and
debits as sets.  If multiple identical charges were made to the same
account via different account designators then the Set eliminates the
duplicates creating an unbalanced journal entry.  For this reason, we
must sum the entries again after they are mapped from designators to
account identifiers.
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 968e9da..8d46e36 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -274,6 +274,7 @@
       final Set<String> accountDesignators,
       final BigDecimal amount,
       final LocalDateTime forDateTime,
+      final int minorCurrencyUnits,
       final CostComponent... expectedCostComponents) {
     final Payment payment = portfolioManager.getCostComponentsForAction(
         productIdentifier,
@@ -283,6 +284,7 @@
         amount,
         DateConverter.toIsoString(forDateTime)
     );
+    payment.getCostComponents().forEach(x -> x.setAmount(x.getAmount().setScale(minorCurrencyUnits, BigDecimal.ROUND_HALF_EVEN)));
     final Set<CostComponent> setOfCostComponents = new HashSet<>(payment.getCostComponents());
     final Set<CostComponent> setOfExpectedCostComponents = Stream.of(expectedCostComponents)
         .filter(x -> x.getAmount().compareTo(BigDecimal.ZERO) != 0)
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 9cb44f4..3bd4dda 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -468,6 +468,22 @@
               journalEntry.getMessage(),
               journalEntry.getTransactionDate(),
               Double.valueOf(debtor.getAmount())));
+
+      final BigDecimal creditorSum = journalEntry.getCreditors().stream()
+          .map(Creditor::getAmount)
+          .map(Double::valueOf)
+          .map(BigDecimal::valueOf)
+          .map(x -> x.setScale(4, BigDecimal.ROUND_HALF_EVEN))
+          .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+      final BigDecimal debtorSum = journalEntry.getDebtors().stream()
+          .map(Debtor::getAmount)
+          .map(Double::valueOf)
+          .map(BigDecimal::valueOf)
+          .map(x -> x.setScale(4, BigDecimal.ROUND_HALF_EVEN))
+          .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+      Assert.assertEquals(creditorSum, debtorSum);
       return null;
     }
   }
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 63b306c..63f71b3 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -393,7 +393,8 @@
         Action.OPEN,
         null,
         null,
-        forDateTime);
+        forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS);
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
@@ -413,7 +414,9 @@
         customerCase.getIdentifier(),
         Action.DENY,
         null,
-        null, forDateTime);
+        null,
+        forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS);
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
@@ -437,7 +440,9 @@
         customerCase.getIdentifier(),
         Action.APPROVE,
         null,
-        null, forDateTime);
+        null,
+        forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS);
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
@@ -480,6 +485,7 @@
         Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP),
         amount,
         forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS,
         new CostComponent(whichDisbursementFee, disbursementFeeAmount),
         new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT),
         new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT),
@@ -585,7 +591,8 @@
         Action.APPLY_INTEREST,
         null,
         null,
-        forDateTime);
+        forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS);
 
     if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
       checkCostComponentForActionCorrect(
@@ -594,7 +601,8 @@
           Action.MARK_LATE,
           null,
           null,
-          forDateTime);
+          forDateTime,
+          MINOR_CURRENCY_UNIT_DIGITS);
     }
     final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
     portfolioBeatListener.publishBeat(interestBeat);
@@ -667,6 +675,7 @@
         new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.LOAN_FUNDS_SOURCE)),
         amount,
         forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS,
         new CostComponent(ChargeIdentifiers.REPAY_PRINCIPAL_ID, principal),
         new CostComponent(ChargeIdentifiers.REPAY_INTEREST_ID, interestAccrued),
         new CostComponent(ChargeIdentifiers.REPAY_FEES_ID, lateFee.add(nonLateFees)),
@@ -735,7 +744,8 @@
         Action.CLOSE,
         null,
         null,
-        forDateTime);
+        forDateTime,
+        MINOR_CURRENCY_UNIT_DIGITS);
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java b/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java
index cc47711..13419dc 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java
@@ -1,3 +1,18 @@
+/*
+ * 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.command;
 
 import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index 23ab654..c513385 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -69,6 +69,24 @@
     this.logger = logger;
   }
 
+  private static class BalanceAdjustment {
+    final private String accountIdentifier; //*Not* designator.
+    final private BigDecimal adjustment;
+
+    BalanceAdjustment(String accountIdentifier, BigDecimal adjustment) {
+      this.accountIdentifier = accountIdentifier;
+      this.adjustment = adjustment;
+    }
+
+    String getAccountIdentifier() {
+      return accountIdentifier;
+    }
+
+    BigDecimal getAdjustment() {
+      return adjustment;
+    }
+  }
+
   public Optional<String> bookCharges(
       final Map<String, BigDecimal> balanceAdjustments,
       final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
@@ -76,18 +94,53 @@
       final String transactionDate,
       final String message,
       final String transactionType) {
+    final String transactionUniqueifier = RandomStringUtils.random(26, true, true);
+    final JournalEntry journalEntry = getJournalEntry(
+        balanceAdjustments,
+        designatorToAccountIdentifierMapper,
+        note,
+        transactionDate,
+        message,
+        transactionType,
+        transactionUniqueifier,
+        UserContextHolder.checkedGetUser());
+
+    //noinspection ConstantConditions
+    if (journalEntry.getCreditors().isEmpty() && journalEntry.getDebtors().isEmpty())
+      return Optional.empty();
+
+    ledgerManager.createJournalEntry(journalEntry);
+    return Optional.of(transactionUniqueifier);
+  }
+
+  static JournalEntry getJournalEntry(
+      final Map<String, BigDecimal> balanceAdjustments,
+      final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
+      final String note,
+      final String transactionDate,
+      final String message,
+      final String transactionType,
+      final String transactionUniqueifier,
+      final String user) {
     final JournalEntry journalEntry = new JournalEntry();
     final Set<Creditor> creditors = new HashSet<>();
     journalEntry.setCreditors(creditors);
     final Set<Debtor> debtors = new HashSet<>();
     journalEntry.setDebtors(debtors);
-    balanceAdjustments.forEach((accountDesignator, balanceAdjustment) -> {
+    final Map<String, BigDecimal> summedBalanceAdjustments = balanceAdjustments.entrySet().stream()
+        .map(entry -> {
+          final String accountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(entry.getKey());
+          return new BalanceAdjustment(accountIdentifier, entry.getValue());
+        })
+        .collect(Collectors.groupingBy(BalanceAdjustment::getAccountIdentifier,
+            Collectors.mapping(BalanceAdjustment::getAdjustment,
+                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
+
+    summedBalanceAdjustments.forEach((accountIdentifier, balanceAdjustment) -> {
       final int sign = balanceAdjustment.compareTo(BigDecimal.ZERO);
       if (sign == 0)
         return;
 
-      final String accountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator);
-
       if (sign < 0) {
         final Debtor debtor = new Debtor();
         debtor.setAccountNumber(accountIdentifier);
@@ -105,23 +158,17 @@
         debtors.isEmpty() && !creditors.isEmpty())
       throw ServiceException.internalError("either only creditors or only debtors were provided.");
 
-    //noinspection ConstantConditions
-    if (creditors.isEmpty() && debtors.isEmpty())
-      return Optional.empty();
 
-    final String transactionUniqueifier = RandomStringUtils.random(26, true, true);
     final String transactionIdentifier = "portfolio." + message + "." + transactionUniqueifier;
     journalEntry.setCreditors(creditors);
     journalEntry.setDebtors(debtors);
-    journalEntry.setClerk(UserContextHolder.checkedGetUser());
+    journalEntry.setClerk(user);
     journalEntry.setTransactionDate(transactionDate);
     journalEntry.setMessage(message);
     journalEntry.setTransactionType(transactionType);
     journalEntry.setNote(note);
     journalEntry.setTransactionIdentifier(transactionIdentifier);
-
-    ledgerManager.createJournalEntry(journalEntry);
-    return Optional.of(transactionUniqueifier);
+    return journalEntry;
   }
 
   public Optional<LocalDateTime> getDateOfOldestEntryContainingMessage(final String accountIdentifier,
diff --git a/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java b/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java
new file mode 100644
index 0000000..69519bb
--- /dev/null
+++ b/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.portfolio.service.internal.util;
+
+import com.google.common.collect.Sets;
+import io.mifos.accounting.api.v1.domain.Creditor;
+import io.mifos.accounting.api.v1.domain.Debtor;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Myrle Krantz
+ */
+public class AccountingAdapterTest {
+  @Test
+  public void getJournalEntryWithMultipleIdenticalChargesMappedToSameAccount() {
+    final BigDecimal two = BigDecimal.valueOf(2);
+    final BigDecimal negativeTwo = two.negate();
+    final Map<String, BigDecimal> balanceAdjustments = new HashMap<>();
+    balanceAdjustments.put("a", BigDecimal.ONE);
+    balanceAdjustments.put("b", BigDecimal.ONE);
+    balanceAdjustments.put("c", negativeTwo);
+
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper = Mockito.mock(DesignatorToAccountIdentifierMapper.class);
+    Mockito.doReturn("a1").when(designatorToAccountIdentifierMapper).mapOrThrow("a");
+    Mockito.doReturn("a1").when(designatorToAccountIdentifierMapper).mapOrThrow("b");
+    Mockito.doReturn("c1").when(designatorToAccountIdentifierMapper).mapOrThrow("c");
+
+    final JournalEntry journalEntry = AccountingAdapter.getJournalEntry(
+        balanceAdjustments,
+        designatorToAccountIdentifierMapper,
+        "", "", "", "", "", "");
+    Assert.assertEquals(Sets.newHashSet(new Debtor("c1", two.toPlainString())), journalEntry.getDebtors());
+    Assert.assertEquals(Sets.newHashSet(new Creditor("a1", two.toPlainString())), journalEntry.getCreditors());
+  }
+
+}
\ No newline at end of file