Merge pull request #46 from myrle-krantz/develop

Made portfolio more testable and corrected error found in integration test.
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 dffbe9f..595d2b4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -19,6 +19,7 @@
 import io.mifos.accounting.api.v1.domain.*;
 import io.mifos.core.api.util.NotFoundException;
 import io.mifos.core.lang.DateConverter;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import org.hamcrest.Description;
 import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentMatcher;
@@ -59,10 +60,10 @@
   static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
   static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
   static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
-  static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="missingInChartOfAccounts";
-  static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "001-008"; //TODO: ??
-  static final String LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER = "001-009"; //TODO: ??
-  static final String ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER = "001-010"; //TODO: ??
+  static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="8530";
+  static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
+  static final String LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER = "7840";
+  static final String ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER = "3010";
 
   static final Map<String, AccountData> accountMap = new HashMap<>();
 
@@ -228,7 +229,7 @@
   private static Account lateFeeIncomeAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LATE_FEE_INCOME_ACCOUNT_IDENTIFIER);
-    //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER); //TODO: ??
+    ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     ret.setType(AccountType.LIABILITY.name()); //TODO: ??
     return ret;
   }
@@ -236,7 +237,7 @@
   private static Account lateFeeAccrualAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER);
-    //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER); //TODO: ??
+    ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.LIABILITY.name()); //TODO: ??
     return ret;
   }
@@ -335,13 +336,18 @@
   private static class JournalEntryMatcher extends ArgumentMatcher<JournalEntry> {
     private final Set<Debtor> debtors;
     private final Set<Creditor> creditors;
+    private final String transactionPrefix;
     private JournalEntry checkedArgument;
 
     private JournalEntryMatcher(final Set<Debtor> debtors,
-                                final Set<Creditor> creditors) {
+                                final Set<Creditor> creditors,
+                                final String productIdentifier,
+                                final String caseIdentifier,
+                                final Action action) {
       this.debtors = debtors;
       this.creditors = creditors;
       this.checkedArgument = null; //Set when matches called.
+      this.transactionPrefix = "portfolio." + productIdentifier + "." + caseIdentifier + "." + action.name();
     }
 
     @Override
@@ -354,7 +360,8 @@
       checkedArgument = (JournalEntry) argument;
 
       return this.debtors.equals(checkedArgument.getDebtors()) &&
-              this.creditors.equals(checkedArgument.getCreditors());
+          this.creditors.equals(checkedArgument.getCreditors()) &&
+          checkedArgument.getTransactionIdentifier().startsWith(transactionPrefix);
     }
 
     @Override
@@ -470,17 +477,24 @@
   static void verifyTransfer(final LedgerManager ledgerManager,
                              final String fromAccountIdentifier,
                              final String toAccountIdentifier,
-                             final BigDecimal amount) {
+                             final BigDecimal amount,
+                             final String productIdentifier,
+                             final String caseIdentifier,
+                             final Action action) {
     final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(
-            Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
-            Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())));
+        Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
+        Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())),
+        productIdentifier, caseIdentifier, action);
     Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
   }
 
   static void verifyTransfer(final LedgerManager ledgerManager,
                              final Set<Debtor> debtors,
-                             final Set<Creditor> creditors) {
-    final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors);
+                             final Set<Creditor> creditors,
+                             final String productIdentifier,
+                             final String caseIdentifier,
+                             final Action action) {
+    final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors, productIdentifier, caseIdentifier, action);
     Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
 
   }
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 785627a..739e354 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -157,7 +157,7 @@
 
     AccountingFixture.verifyTransfer(ledgerManager,
         AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, AccountingFixture.PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER,
-        PROCESSING_FEE_AMOUNT
+        PROCESSING_FEE_AMOUNT, product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN
     );
   }
 
@@ -203,7 +203,7 @@
     final Set<Creditor> creditors = new HashSet<>();
     creditors.add(new Creditor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
     creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
-    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE);
 
     expectedCurrentBalance = BigDecimal.ZERO;
   }
@@ -232,7 +232,7 @@
     creditors.add(new Creditor(customerLoanAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
     creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
     creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
-    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE);
 
     expectedCurrentBalance = expectedCurrentBalance.add(caseParameters.getMaximumBalance());
   }
@@ -243,7 +243,7 @@
     final String beatIdentifier = "alignment0";
     final String midnightTimeStamp = DateConverter.toIsoString(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS));
 
-    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
 
     final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
     portfolioBeatListener.publishBeat(interestBeat);
@@ -270,7 +270,7 @@
     creditors.add(new Creditor(
         AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
         calculatedInterest.toPlainString()));
-    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST);
 
     expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
   }
@@ -278,7 +278,7 @@
   private void step7PaybackPartialAmount(final BigDecimal amount) throws InterruptedException {
     logger.info("step7PaybackPartialAmount");
 
-    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
 
     checkStateTransfer(
         product.getIdentifier(),
@@ -305,7 +305,7 @@
     if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
       creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
 
-    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT);
 
     expectedCurrentBalance = expectedCurrentBalance.subtract(amount);
     interestAccrued = BigDecimal.ZERO;
@@ -314,7 +314,7 @@
   private void step8Close() throws InterruptedException {
     logger.info("step8Close");
 
-    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
 
     checkStateTransfer(
         product.getIdentifier(),
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 d84fc63..cc20d09 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
@@ -186,7 +186,7 @@
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
     final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
-    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
 
     if (dataContextOfAction.getCaseParameters().getMaximumBalance().compareTo(
         currentBalance.add(requestedDisbursalSize)) < 0)
@@ -237,7 +237,7 @@
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
     final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
-    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
 
     final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
 
@@ -277,7 +277,7 @@
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
     final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
-    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
 
     final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
 
@@ -377,7 +377,7 @@
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
     final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
-    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
     if (currentBalance.compareTo(BigDecimal.ZERO) != 0)
       throw ServiceException.conflict("Cannot close loan until the balance is zero.");
 
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
index 7173e76..6693e03 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
@@ -145,11 +145,18 @@
               .map(ChargeDefinitionMapper::map)
               .collect(Collectors.toList());
 
-      if (!AccountingAdapter.accountAssignmentsCoverChargeDefinitions(accountAssignments, chargeDefinitions))
-        throw ServiceException.conflict("Not ready to enable product '" + changeEnablingOfProductCommand.getProductIdentifier() + "'. One or more of the charge definitions contains a designator for which no account assignment exists.");
+      final Set<String> accountAssignmentsRequiredButNotProvided
+          = AccountingAdapter.accountAssignmentsRequiredButNotProvided(accountAssignments, chargeDefinitions);
+      if (!accountAssignmentsRequiredButNotProvided.isEmpty())
+        throw ServiceException.conflict("Not ready to enable product ''{0}''. One or more of the charge definitions " +
+            "contains a designator for which no account assignment exists. Here are the unassigned designators ''{1}''",
+            changeEnablingOfProductCommand.getProductIdentifier(), accountAssignmentsRequiredButNotProvided);
 
-      if (!accountingAdapter.accountAssignmentsRepresentRealAccounts(accountAssignments))
-        throw ServiceException.conflict("Not ready to enable product '" + changeEnablingOfProductCommand.getProductIdentifier() + "'. One or more of the account assignments points to an account or ledger which does not exist.");
+      final Set<String> accountAssignmentsMappedToNonexistentAccounts = accountingAdapter.accountAssignmentsMappedToNonexistentAccounts(accountAssignments);
+      if (!accountAssignmentsMappedToNonexistentAccounts.isEmpty())
+        throw ServiceException.conflict("Not ready to enable product ''{0}''. The following account assignments point " +
+            "to an account or ledger which does not exist ''{1}''.", changeEnablingOfProductCommand.getProductIdentifier(),
+            accountAssignmentsMappedToNonexistentAccounts);
     }
 
     productEntity.setEnabled(changeEnablingOfProductCommand.getEnabled());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
index 7d8b475..5d0d92b 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
@@ -115,7 +115,7 @@
     final Product product = maybeProduct.get();
     final Set<AccountAssignment> accountAssignments = product.getAccountAssignments();
     final List<ChargeDefinition> chargeDefinitions = chargeDefinitionService.findAllEntities(identifier);
-    return AccountingAdapter.accountAssignmentsCoverChargeDefinitions(accountAssignments, chargeDefinitions);
+    return AccountingAdapter.accountAssignmentsRequiredButNotProvided(accountAssignments, chargeDefinitions).isEmpty();
   }
 
   public Set<AccountAssignment> getIncompleteAccountAssignments(final String identifier) {
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 341d2d7..bb15c70 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
@@ -84,7 +84,7 @@
     journalEntry.setMessage(message);
     journalEntry.setTransactionType(transactionType);
     journalEntry.setNote(note);
-    journalEntry.setTransactionIdentifier("bastet" + RandomStringUtils.random(26, true, true));
+    journalEntry.setTransactionIdentifier("portfolio." + message + "." + RandomStringUtils.random(26, true, true));
 
     ledgerManager.createJournalEntry(journalEntry);
   }
@@ -192,14 +192,18 @@
   }
 
 
-  public static boolean accountAssignmentsCoverChargeDefinitions(
+  public static Set<String> accountAssignmentsRequiredButNotProvided(
           final Set<AccountAssignment> accountAssignments,
           final List<ChargeDefinition> chargeDefinitionEntities) {
     final Set<String> allAccountDesignatorsRequired = getRequiredAccountDesignators(chargeDefinitionEntities);
     final Set<String> allAccountDesignatorsDefined = accountAssignments.stream().map(AccountAssignment::getDesignator)
             .collect(Collectors.toSet());
-    return allAccountDesignatorsDefined.containsAll(allAccountDesignatorsRequired);
-
+    if (allAccountDesignatorsDefined.containsAll(allAccountDesignatorsRequired))
+      return Collections.emptySet();
+    else {
+      allAccountDesignatorsRequired.removeAll(allAccountDesignatorsDefined);
+      return allAccountDesignatorsRequired;
+    }
   }
 
   public static Set<String> getRequiredAccountDesignators(final Collection<ChargeDefinition> chargeDefinitionEntities) {
@@ -224,9 +228,12 @@
       retBuilder.add(accountDesignator);
   }
 
-  public boolean accountAssignmentsRepresentRealAccounts(final Set<AccountAssignment> accountAssignments)
+  public Set<String> accountAssignmentsMappedToNonexistentAccounts(final Set<AccountAssignment> accountAssignments)
   {
-    return accountAssignments.stream().allMatch(this::accountAssignmentRepresentsRealAccount);
+    return accountAssignments.stream()
+        .filter(x -> !accountAssignmentRepresentsRealAccount(x))
+        .map(AccountAssignment::getDesignator)
+        .collect(Collectors.toSet());
   }
 
   public boolean accountAssignmentRepresentsRealAccount(final AccountAssignment accountAssignment) {