Merge pull request #30 from myrle-krantz/develop

apply interest and deny
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
index 6157f37..30c4208 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
@@ -17,6 +17,7 @@
 
 import io.mifos.portfolio.api.v1.domain.CostComponent;
 
+import javax.annotation.Nullable;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.Objects;
@@ -29,7 +30,7 @@
   private Double interestRate;
   private List<CostComponent> costComponents;
   private BigDecimal remainingPrincipal;
-  private String date;
+  private @Nullable String date;
 
   public PlannedPayment() {
   }
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanCommandEvent.java b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanCommandEvent.java
index 72e0d50..2ae1249 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanCommandEvent.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanCommandEvent.java
@@ -24,15 +24,13 @@
 public class IndividualLoanCommandEvent {
   private String productIdentifier;
   private String caseIdentifier;
-  private String commandIdentifier;
 
   public IndividualLoanCommandEvent() {
   }
 
-  public IndividualLoanCommandEvent(String productIdentifier, String caseIdentifier, String commandIdentifier) {
+  public IndividualLoanCommandEvent(String productIdentifier, String caseIdentifier) {
     this.productIdentifier = productIdentifier;
     this.caseIdentifier = caseIdentifier;
-    this.commandIdentifier = commandIdentifier;
   }
 
   public String getProductIdentifier() {
@@ -51,27 +49,18 @@
     this.caseIdentifier = caseIdentifier;
   }
 
-  public String getCommandIdentifier() {
-    return commandIdentifier;
-  }
-
-  public void setCommandIdentifier(String commandIdentifier) {
-    this.commandIdentifier = commandIdentifier;
-  }
-
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     IndividualLoanCommandEvent that = (IndividualLoanCommandEvent) o;
     return Objects.equals(productIdentifier, that.productIdentifier) &&
-            Objects.equals(caseIdentifier, that.caseIdentifier) &&
-            Objects.equals(commandIdentifier, that.commandIdentifier);
+            Objects.equals(caseIdentifier, that.caseIdentifier);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(productIdentifier, caseIdentifier, commandIdentifier);
+    return Objects.hash(productIdentifier, caseIdentifier);
   }
 
   @Override
@@ -79,7 +68,6 @@
     return "IndividualLoanCommandEvent{" +
             "productIdentifier='" + productIdentifier + '\'' +
             ", caseIdentifier='" + caseIdentifier + '\'' +
-            ", commandIdentifier='" + commandIdentifier + '\'' +
             '}';
   }
 }
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index c76e05a..3814efe 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -201,7 +201,7 @@
   void changeChargeDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
           @PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier,
-          final ChargeDefinition taskDefinition);
+          final ChargeDefinition chargeDefinition);
 
   @RequestMapping(
           value = "/products/{productidentifier}/charges/{chargedefinitionidentifier}",
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
index a7f1ffc..8cd4fc0 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
@@ -15,6 +15,7 @@
  */
 package io.mifos.portfolio.api.v1.domain;
 
+import io.mifos.portfolio.api.v1.validation.ValidChargeReference;
 import io.mifos.portfolio.api.v1.validation.ValidPaymentCycleUnit;
 import io.mifos.core.lang.validation.constraints.ValidIdentifier;
 import org.hibernate.validator.constraints.NotBlank;
@@ -61,7 +62,7 @@
   @NotNull
   private ChargeMethod chargeMethod;
 
-  @ValidIdentifier(optional = true)
+  @ValidChargeReference
   private String proportionalTo;
 
   @ValidIdentifier
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckAccountAssignments.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckAccountAssignments.java
index b579e03..e92cece 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckAccountAssignments.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckAccountAssignments.java
@@ -26,7 +26,6 @@
 /**
  * @author Myrle Krantz
  */
-@SuppressWarnings("WeakerAccess")
 public class CheckAccountAssignments implements ConstraintValidator<ValidAccountAssignments, Set<AccountAssignment>> {
   public CheckAccountAssignments() {
   }
@@ -42,9 +41,8 @@
             .map(AccountAssignment::getDesignator)
             .collect(Collectors.toList());
 
-    boolean allValidAccountAssignments = !(accountAssignments.stream()
-            .filter(x -> !isValidAccountAssignment(x))
-            .findAny().isPresent());
+    final boolean allValidAccountAssignments = accountAssignments.stream()
+        .allMatch(this::isValidAccountAssignment);
     if (!allValidAccountAssignments)
       return false;
 
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java
new file mode 100644
index 0000000..ab16e8b
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckChargeReference.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.api.v1.validation;
+
+import io.mifos.core.lang.validation.CheckIdentifier;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CheckChargeReference implements ConstraintValidator<ValidChargeReference, String> {
+
+  @Override
+  public void initialize(ValidChargeReference constraintAnnotation) {
+
+  }
+
+  @Override
+  public boolean isValid(String value, ConstraintValidatorContext context) {
+    if (value == null)
+      return true;
+
+    if (value.equals(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR) ||
+        value.equals(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR))
+      return true;
+
+    final CheckIdentifier identifierChecker = new CheckIdentifier();
+    return identifierChecker.isValid(value, context);
+  }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeReference.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeReference.java
new file mode 100644
index 0000000..130ac59
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeReference.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.api.v1.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+    validatedBy = {CheckChargeReference.class}
+)
+public @interface ValidChargeReference {
+  String message() default "Only valid identifiers or {delegates} can be charge references.";
+
+  Class<?>[] groups() default {};
+
+  Class<? extends Payload>[] payload() default {};
+}
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
index 0838319..750be0b 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
@@ -103,6 +103,12 @@
               x.setProportionalTo(null);
             })
             .valid(true));
+    ret.add(new ValidationTestCase<ChargeDefinition>("proportionalToRunningBalance")
+        .adjustment(x -> x.setProportionalTo("{runningbalance}"))
+        .valid(true));
+    ret.add(new ValidationTestCase<ChargeDefinition>("proportionalToMaximumBalance")
+        .adjustment(x -> x.setProportionalTo("{maximumbalance}"))
+        .valid(true));
     return ret;
   }
 }
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 4e7a98e..4e27123 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -18,10 +18,7 @@
 import io.mifos.accounting.api.v1.client.LedgerManager;
 import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
 import io.mifos.core.api.context.AutoUserContext;
-import io.mifos.core.test.env.TestEnvironment;
 import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
-import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
-import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
 import io.mifos.core.test.listener.EnableEventRecording;
 import io.mifos.core.test.listener.EventRecorder;
 import io.mifos.individuallending.api.v1.client.IndividualLending;
@@ -37,8 +34,6 @@
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 import io.mifos.portfolio.service.internal.util.RhythmAdapter;
 import org.junit.*;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
@@ -76,8 +71,7 @@
 @RunWith(SpringRunner.class)
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
         classes = {AbstractPortfolioTest.TestConfiguration.class})
-public class AbstractPortfolioTest {
-  private static final String APP_NAME = "portfolio-v1";
+public class AbstractPortfolioTest extends SuiteTestEnvironment {
   private static final String LOGGER_NAME = "test-logger";
 
   @Configuration
@@ -107,17 +101,9 @@
 
   static final String TEST_USER = "setau";
 
-  final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
-  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
-  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
-  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
-
   @ClassRule
-  public static TestRule orderClassRules = RuleChain
-          .outerRule(testEnvironment)
-          .around(cassandraInitializer)
-          .around(mariaDBInitializer)
-          .around(tenantDataStoreContext);
+  public final static TenantDataStoreContextTestRule tenantDataStoreContext
+      = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
 
   @Rule
   public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
@@ -213,8 +199,7 @@
     command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
     portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
 
-    Assert.assertTrue(eventRecorder.waitForMatch(event,
-            (IndividualLoanCommandEvent x) -> individualLoanCommandEventMatches(x, productIdentifier, caseIdentifier)));
+    Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier)));
 
     final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
     Assert.assertEquals(customerCase.getCurrentState(), nextState.name());
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 89f9ccb..d838179 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -20,14 +20,15 @@
 import org.hamcrest.Description;
 import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentMatcher;
+import org.mockito.Matchers;
 import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import javax.validation.Validation;
 import javax.validation.Validator;
 import java.math.BigDecimal;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Set;
+import java.util.*;
 
 import static org.mockito.Matchers.argThat;
 
@@ -53,6 +54,8 @@
   static final String LOAN_INTEREST_ACCRUAL_ACCOUNT = "7810";
   static final String CONSUMER_LOAN_INTEREST_ACCOUNT = "1103";
 
+  static final Map<String, Account> accountMap = new HashMap<>();
+
 
   private static Ledger cashLedger() {
     final Ledger ret = new Ledger();
@@ -111,60 +114,60 @@
 
   }
 
-  private static Account loanFundsSourceAccount() {
+  private static void loanFundsSourceAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
     ret.setLedger(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    return ret;
+    accountMap.put(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, ret);
   }
 
-  private static Account processingFeeIncomeAccount() {
+  private static void processingFeeIncomeAccount() {
     final Account ret = new Account();
     ret.setIdentifier(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
     ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
-    return ret;
+    accountMap.put(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
   }
 
-  private static Account loanOriginationFeesIncomeAccount() {
+  private static void loanOriginationFeesIncomeAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
     ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
-    return ret;
+    accountMap.put(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, ret);
   }
 
-  private static Account disbursementFeeIncomeAccount() {
+  private static void disbursementFeeIncomeAccount() {
     final Account ret = new Account();
     ret.setIdentifier(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER);
     ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
-    return ret;
+    accountMap.put(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
   }
 
-  private static Account tellerOneAccount() {
+  private static void tellerOneAccount() {
     final Account ret = new Account();
     ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
     ret.setLedger(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    return ret;
+    accountMap.put(TELLER_ONE_ACCOUNT_IDENTIFIER, ret);
   }
 
-  private static Account loanInterestAccrualAccount() {
+  private static void loanInterestAccrualAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LOAN_INTEREST_ACCRUAL_ACCOUNT);
     ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    return ret;
+    accountMap.put(LOAN_INTEREST_ACCRUAL_ACCOUNT, ret);
   }
 
-  private static Account consumerLoanInterestAccount() {
+  private static void consumerLoanInterestAccount() {
     final Account ret = new Account();
     ret.setIdentifier(CONSUMER_LOAN_INTEREST_ACCOUNT);
     ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
-    return ret;
+    accountMap.put(CONSUMER_LOAN_INTEREST_ACCOUNT, ret);
   }
 
   private static AccountPage customerLoanAccountsPage() {
@@ -290,7 +293,32 @@
     }
   }
 
+  private static class FindAccountAnswer implements Answer {
+    @Override
+    public Account answer(InvocationOnMock invocation) throws Throwable {
+      final String identifier = invocation.getArgumentAt(0, String.class);
+      return accountMap.get(identifier);
+    }
+  }
+
+  private static class CreateAccountAnswer implements Answer {
+    @Override
+    public Void answer(InvocationOnMock invocation) throws Throwable {
+      final Account account = invocation.getArgumentAt(0, Account.class);
+      accountMap.put(account.getIdentifier(), account);
+      return null;
+    }
+  }
+
   static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
+    loanFundsSourceAccount();
+    loanOriginationFeesIncomeAccount();
+    processingFeeIncomeAccount();
+    disbursementFeeIncomeAccount();
+    tellerOneAccount();
+    loanInterestAccrualAccount();
+    consumerLoanInterestAccount();
+
     Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
     Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     Mockito.doReturn(cashLedger()).when(ledgerManagerMock).findLedger(CASH_LEDGER_IDENTIFIER);
@@ -298,17 +326,17 @@
     Mockito.doReturn(customerLoanLedger()).when(ledgerManagerMock).findLedger(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
     Mockito.doReturn(loanIncomeLedger()).when(ledgerManagerMock).findLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
     Mockito.doReturn(accruedIncomeLedger()).when(ledgerManagerMock).findLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
-    Mockito.doReturn(loanFundsSourceAccount()).when(ledgerManagerMock).findAccount(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
-    Mockito.doReturn(loanOriginationFeesIncomeAccount()).when(ledgerManagerMock).findAccount(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
-    Mockito.doReturn(processingFeeIncomeAccount()).when(ledgerManagerMock).findAccount(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
-    Mockito.doReturn(disbursementFeeIncomeAccount()).when(ledgerManagerMock).findAccount(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER);
-    Mockito.doReturn(tellerOneAccount()).when(ledgerManagerMock).findAccount(TELLER_ONE_ACCOUNT_IDENTIFIER);
-    Mockito.doReturn(loanInterestAccrualAccount()).when(ledgerManagerMock).findAccount(LOAN_INTEREST_ACCRUAL_ACCOUNT);
-    Mockito.doReturn(consumerLoanInterestAccount()).when(ledgerManagerMock).findAccount(CONSUMER_LOAN_INTEREST_ACCOUNT);
     Mockito.doReturn(customerLoanAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(CUSTOMER_LOAN_LEDGER_IDENTIFIER),
             Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
     Mockito.doReturn(pendingDisbursalAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(PENDING_DISBURSAL_LEDGER_IDENTIFIER),
             Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(new FindAccountAnswer()).when(ledgerManagerMock).findAccount(Matchers.anyString());
+    Mockito.doAnswer(new CreateAccountAnswer()).when(ledgerManagerMock).createAccount(Matchers.any());
+  }
+
+  static void mockBalance(final String accountIdentifier, final BigDecimal balance) {
+    accountMap.get(accountIdentifier).setBalance(balance.doubleValue());
   }
 
   static String verifyAccountCreation(final LedgerManager ledgerManager,
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index 5d60486..088bdbe 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -23,6 +23,7 @@
 import io.mifos.individuallending.api.v1.domain.product.ProductParameters;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.function.Consumer;
@@ -37,8 +38,10 @@
 
 @SuppressWarnings({"WeakerAccess", "unused"})
 public class Fixture {
+  static final int MINOR_CURRENCY_UNIT_DIGITS = 2;
+  static final BigDecimal INTEREST_RATE = BigDecimal.valueOf(0.10).setScale(4, RoundingMode.HALF_EVEN);
+  static final BigDecimal ACCRUAL_PERIODS = BigDecimal.valueOf(365.2425);
 
-  public static final int MINOR_CURRENCY_UNIT_DIGITS = 2;
   private static int uniquenessSuffix = 0;
 
   static public Product getTestProduct() {
diff --git a/component-test/src/main/java/io/mifos/portfolio/SuiteTestEnvironment.java b/component-test/src/main/java/io/mifos/portfolio/SuiteTestEnvironment.java
new file mode 100644
index 0000000..3b84ee0
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/SuiteTestEnvironment.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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;
+
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+import org.junit.ClassRule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.rules.RunExternalResourceOnce;
+
+/**
+ * @author Myrle Krantz
+ */
+public class SuiteTestEnvironment {
+  static final String APP_NAME = "portfolio-v1";
+  static final TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  static final CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  static final MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+      .outerRule(new RunExternalResourceOnce(testEnvironment))
+      .around(new RunExternalResourceOnce(cassandraInitializer))
+      .around(new RunExternalResourceOnce(mariaDBInitializer));
+}
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 7ec81c9..ecf8a64 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -19,35 +19,45 @@
 import io.mifos.accounting.api.v1.domain.AccountType;
 import io.mifos.accounting.api.v1.domain.Creditor;
 import io.mifos.accounting.api.v1.domain.Debtor;
+import io.mifos.core.api.util.ApiFactory;
+import io.mifos.core.lang.DateConverter;
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
 import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
+import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
 import io.mifos.portfolio.api.v1.domain.Case;
+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.api.v1.events.ChargeDefinitionEvent;
 import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.rhythm.spi.v1.client.BeatListener;
+import io.mifos.rhythm.spi.v1.domain.BeatPublish;
+import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
 import org.junit.Assert;
-import org.junit.FixMethodOrder;
+import org.junit.Before;
 import org.junit.Test;
-import org.junit.runners.MethodSorters;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
-import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
 import static io.mifos.portfolio.Fixture.MINOR_CURRENCY_UNIT_DIGITS;
 
 /**
  * @author Myrle Krantz
  */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class TestAccountingInteractionInLoanWorkflow extends AbstractPortfolioTest {
   private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(10_0000, MINOR_CURRENCY_UNIT_DIGITS);
   private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_0000, MINOR_CURRENCY_UNIT_DIGITS);
   private static final BigDecimal DISBURSEMENT_FEE_AMOUNT = BigDecimal.valueOf(1_0000, MINOR_CURRENCY_UNIT_DIGITS);
 
+  private BeatListener portfolioBeatListener;
+
   private static Product product;
   private static Case customerCase;
   private static CaseParameters caseParameters;
@@ -55,22 +65,52 @@
   private static String customerLoanAccountIdentifier;
 
 
+  @Before
+  public void prepBeatListener() {
+    portfolioBeatListener = new ApiFactory(logger).create(BeatListener.class, testEnvironment.serverURI());
+  }
+
   @Test
-  public void step1CreateProduct() throws InterruptedException {
-    //Create product and set charges to fixed fees.
+  public void workflowTerminatingInApplicationDenial() throws InterruptedException {
+    step1CreateProduct();
+    step2CreateCase();
+    step3OpenCase();
+    step4DenyCase();
+  }
+
+  @Test
+  public void workflowTerminatingInEarlyLoanPayoff() throws InterruptedException {
+    step1CreateProduct();
+    step2CreateCase();
+    step3OpenCase();
+    step4ApproveCase();
+    step5DisburseFullAmount();
+    step6CalculateInterestAccrual();
+    //step7PaybackFullAmount();
+  }
+
+  //Create product and set charges to fixed fees.
+  private void step1CreateProduct() throws InterruptedException {
+    logger.info("step1CreateProduct");
     product = createProduct();
 
     setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT);
     setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT);
     setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID, DISBURSEMENT_FEE_AMOUNT);
 
+    final ChargeDefinition interestChargeDefinition = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.INTEREST_ID);
+    interestChargeDefinition.setAmount(Fixture.INTEREST_RATE);
+
+    portfolioManager.changeChargeDefinition(product.getIdentifier(), interestChargeDefinition.getIdentifier(), interestChargeDefinition);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
+        new ChargeDefinitionEvent(product.getIdentifier(), interestChargeDefinition.getIdentifier())));
+
     portfolioManager.enableProduct(product.getIdentifier(), true);
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
   }
 
-  @Test
-  public void step2CreateCase() throws InterruptedException {
-    //Create case.
+  private void step2CreateCase() throws InterruptedException {
+    logger.info("step2CreateCase");
     caseParameters = Fixture.createAdjustedCaseParameters(x -> {
     });
     final String caseParametersAsString = new Gson().toJson(caseParameters);
@@ -82,14 +122,14 @@
   }
 
   //Open the case and accept a processing fee.
-  @Test
-  public void step3OpenCase() throws InterruptedException {
+  private void step3OpenCase() throws InterruptedException {
+    logger.info("step3OpenCase");
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
         Action.OPEN,
         Collections.singletonList(assignEntryToTeller()),
-        OPEN_INDIVIDUALLOAN_CASE,
+        IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
         Case.State.PENDING);
     checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
     checkCostComponentForActionCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE,
@@ -103,15 +143,29 @@
   }
 
 
+  //Deny the case. Once this is done, no more actions are possible for the case.
+  private void step4DenyCase() throws InterruptedException {
+    logger.info("step4DenyCase");
+    checkStateTransfer(
+        product.getIdentifier(),
+        customerCase.getIdentifier(),
+        Action.DENY,
+        Collections.singletonList(assignEntryToTeller()),
+        IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE,
+        Case.State.CLOSED);
+    checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
+  }
+
+
   //Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
-  @Test
-  public void step4ApproveCase() throws InterruptedException {
+  private void step4ApproveCase() throws InterruptedException {
+    logger.info("step4ApproveCase");
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
         Action.APPROVE,
         Collections.singletonList(assignEntryToTeller()),
-        APPROVE_INDIVIDUALLOAN_CASE,
+        IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE,
         Case.State.APPROVED);
     checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
 
@@ -131,14 +185,14 @@
   }
 
   //Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
-  @Test
-  public void step5DisburseFullAmount() throws InterruptedException {
+  private void step5DisburseFullAmount() throws InterruptedException {
+    logger.info("step5DisburseFullAmount");
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
         Action.DISBURSE,
         Collections.singletonList(assignEntryToTeller()),
-        DISBURSE_INDIVIDUALLOAN_CASE,
+        IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE,
         Case.State.ACTIVE);
     checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
         Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
@@ -154,4 +208,39 @@
     AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
 
   }
+
+  //Perform daily interest calculation.
+  private void step6CalculateInterestAccrual() throws InterruptedException {
+    logger.info("step6CalculateInterestAccrual");
+    final String beatIdentifier = "alignment0";
+    final String midnightTimeStamp = DateConverter.toIsoString(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS));
+
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, caseParameters.getMaximumBalance());
+
+    final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
+    portfolioBeatListener.publishBeat(interestBeat);
+    Assert.assertTrue(this.eventRecorder.wait(io.mifos.rhythm.spi.v1.events.EventConstants.POST_PUBLISHEDBEAT,
+        new BeatPublishEvent(EventConstants.DESTINATION, beatIdentifier, midnightTimeStamp)));
+
+    Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE,
+        new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier())));
+
+    final Case customerCaseAfterStateChange = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
+    Assert.assertEquals(customerCaseAfterStateChange.getCurrentState(), Case.State.ACTIVE.name());
+
+    final String calculatedInterest = caseParameters.getMaximumBalance().multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
+        .setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN)
+        .toPlainString();
+
+    final Set<Debtor> debtors = new HashSet<>();
+    debtors.add(new Debtor(
+        AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT,
+        calculatedInterest));
+
+    final Set<Creditor> creditors = new HashSet<>();
+    creditors.add(new Creditor(
+        customerLoanAccountIdentifier,
+        calculatedInterest));
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+  }
 }
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
index b67db68..293abf2 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -66,6 +66,7 @@
 
     try {
       portfolioManager.getChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier());
+      //noinspection ConstantConditions
       Assert.assertFalse(true);
     }
     catch (final NotFoundException ignored) { }
@@ -92,4 +93,26 @@
     final ChargeDefinition chargeDefinitionAsCreated = portfolioManager.getChargeDefinition(product.getIdentifier(), chargeDefinition.getIdentifier());
     Assert.assertEquals(chargeDefinition, chargeDefinitionAsCreated);
   }
+
+
+  @Test
+  public void shouldChangeInterestChargeDefinition() throws InterruptedException {
+    final Product product = createProduct();
+
+    final ChargeDefinition interestChargeDefinition
+        = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.INTEREST_ID);
+    interestChargeDefinition.setAmount(Fixture.INTEREST_RATE);
+
+    portfolioManager.changeChargeDefinition(
+        product.getIdentifier(),
+        interestChargeDefinition.getIdentifier(),
+        interestChargeDefinition);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
+        new ChargeDefinitionEvent(product.getIdentifier(), interestChargeDefinition.getIdentifier())));
+
+    final ChargeDefinition chargeDefinitionAsChanged
+        = portfolioManager.getChargeDefinition(product.getIdentifier(), interestChargeDefinition.getIdentifier());
+
+    Assert.assertEquals(interestChargeDefinition, chargeDefinitionAsChanged);
+  }
 }
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index dae6f62..96944a7 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -151,6 +151,20 @@
   }
 
   @Test
+  public void testApproveBeforeOpen() throws InterruptedException {
+    final Product product = createAndEnableProduct();
+    final Case customerCase = createCase(product.getIdentifier());
+
+    checkStateTransferFails(
+        product.getIdentifier(),
+        customerCase.getIdentifier(),
+        Action.APPROVE,
+        Collections.singletonList(assignEntryToTeller()),
+        APPROVE_INDIVIDUALLOAN_CASE,
+        Case.State.CREATED);
+  }
+
+  @Test
   public void testDisburseBeforeApproval() throws InterruptedException {
     final Product product = createAndEnableProduct();
     final Case customerCase = createCase(product.getIdentifier());
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
index 4b73bf3..876d07a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
@@ -331,7 +331,7 @@
     product.setName(StringUtils.repeat("x", 256));
     product.setDescription(StringUtils.repeat("x", 4096));
     product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
-    product.setBalanceRange(new BalanceRange(Fixture.fixScale(BigDecimal.ZERO), Fixture.fixScale(new BigDecimal(10000))));
+    product.setBalanceRange(new BalanceRange(BigDecimal.ZERO.setScale(4, BigDecimal.ROUND_UNNECESSARY), new BigDecimal(10000).setScale(4, BigDecimal.ROUND_UNNECESSARY)));
     product.setInterestRange(new InterestRange(new BigDecimal("999.98"), new BigDecimal("999.99")));
     product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
     product.setCurrencyCode("XTS");
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestSuite.java b/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
new file mode 100644
index 0000000..4fba15b
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestAccountingInteractionInLoanWorkflow.class,
+    TestCases.class,
+    TestChargeDefinitions.class,
+    TestCommands.class,
+    TestIndividualLoans.class,
+    TestPatterns.class,
+    TestProducts.class,
+    TestTaskDefinitions.class
+})
+public class TestSuite extends SuiteTestEnvironment {
+}
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/BeatPublishEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/BeatPublishEventListener.java
new file mode 100644
index 0000000..7251ff7
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/BeatPublishEventListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.rhythm.spi.v1.events.EventConstants;
+import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class BeatPublishEventListener {
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public BeatPublishEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = io.mifos.portfolio.api.v1.events.EventConstants.DESTINATION,
+      destination = io.mifos.portfolio.api.v1.events.EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_POST_PUBLISHEDBEAT
+  )
+  public void onPublishBeat(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                           final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.POST_PUBLISHEDBEAT, payload, BeatPublishEvent.class);
+  }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 14aa235..6be1dae 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -16,6 +16,7 @@
 package io.mifos.individuallending;
 
 import com.google.gson.Gson;
+import io.mifos.core.lang.ServiceException;
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
 import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
@@ -25,6 +26,7 @@
 import io.mifos.individuallending.internal.repository.CaseParametersRepository;
 import io.mifos.individuallending.internal.repository.CreditWorthinessFactorType;
 import io.mifos.individuallending.internal.service.CostComponentService;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
 import io.mifos.portfolio.api.v1.domain.Case;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
 import io.mifos.portfolio.api.v1.domain.CostComponent;
@@ -145,14 +147,14 @@
     writeOffAllowanceCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
 
     final ChargeDefinition interestCharge = charge(
-            INTEREST_NAME,
-            Action.ACCEPT_PAYMENT,
-            BigDecimal.valueOf(0.05),
-            CUSTOMER_LOAN,
-            PENDING_DISBURSAL);
+        INTEREST_NAME,
+        Action.ACCEPT_PAYMENT,
+        BigDecimal.valueOf(0.05),
+        INTEREST_ACCRUAL,
+        PENDING_DISBURSAL);
     interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
     interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
-    interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
+    interestCharge.setAccrualAccountDesignator(CUSTOMER_LOAN);
     interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
 
     final ChargeDefinition disbursementReturnCharge = charge(
@@ -263,13 +265,26 @@
           final String productIdentifier,
           final String caseIdentifier,
           final String actionIdentifier) {
-    return costComponentService.getCostComponents(productIdentifier, caseIdentifier, Action.valueOf(actionIdentifier))
+    final Action action = Action.valueOf(actionIdentifier);
+    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
+    final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState());
+    checkActionCanBeExecuted(caseState, action);
+
+    return costComponentService.getCostComponentsForAction(action, dataContextOfAction)
+        .stream()
+        .map(costComponentEntry -> new CostComponent(costComponentEntry.getKey().getIdentifier(), costComponentEntry.getValue().getAmount()))
+        .collect(Collectors.toList())
             .stream()
             .map(x -> new CostComponent(x.getChargeIdentifier(), x.getAmount()))
             .collect(Collectors.toList());
   }
 
-  public static Set<Action> getAllowedNextActionsForState(Case.State state) {
+  public static void checkActionCanBeExecuted(final Case.State state, final Action action) {
+    if (!getAllowedNextActionsForState(state).contains(action))
+      throw ServiceException.badRequest("Cannot call action {0} from state {1}", action.name(), state.name());
+  }
+
+  private static Set<Action> getAllowedNextActionsForState(final Case.State state) {
     switch (state)
     {
       case CREATED:
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/ApplyInterestCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/ApplyInterestCommand.java
new file mode 100644
index 0000000..1e658f3
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/ApplyInterestCommand.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ApplyInterestCommand {
+  private final String productIdentifier;
+  private final String caseIdentifier;
+
+  public ApplyInterestCommand(String productIdentifier, String caseIdentifier) {
+    this.productIdentifier = productIdentifier;
+    this.caseIdentifier = caseIdentifier;
+  }
+
+  public String getProductIdentifier() {
+    return productIdentifier;
+  }
+
+  public String getCaseIdentifier() {
+    return caseIdentifier;
+  }
+
+  @Override
+  public String toString() {
+    return "ApplyInterestCommand{" +
+        "productIdentifier='" + productIdentifier + '\'' +
+        ", caseIdentifier='" + caseIdentifier + '\'' +
+        '}';
+  }
+}
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
new file mode 100644
index 0000000..af4abc5
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.CommandLogLevel;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.command.internal.CommandBus;
+import io.mifos.core.lang.ApplicationName;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.individuallending.internal.command.ApplyInterestCommand;
+import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.service.config.PortfolioProperties;
+import io.mifos.portfolio.service.internal.command.CreateBeatPublishCommand;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.rhythm.spi.v1.domain.BeatPublish;
+import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
+import io.mifos.rhythm.spi.v1.events.EventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Aggregate
+public class BeatPublishCommandHandler {
+  private final CaseRepository caseRepository;
+  private final PortfolioProperties portfolioProperties;
+  private final ApplicationName applicationName;
+  private final CommandBus commandBus;
+
+  @Autowired
+  public BeatPublishCommandHandler(
+      final CaseRepository caseRepository,
+      final PortfolioProperties portfolioProperties,
+      final ApplicationName applicationName,
+      final CommandBus commandBus) {
+    this.caseRepository = caseRepository;
+    this.portfolioProperties = portfolioProperties;
+    this.applicationName = applicationName;
+    this.commandBus = commandBus;
+  }
+
+  @Transactional
+  @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_PUBLISHEDBEAT)
+  public BeatPublishEvent process(final CreateBeatPublishCommand createBeatPublishCommand) {
+    final BeatPublish instance = createBeatPublishCommand.getInstance();
+    final LocalDateTime forTime = DateConverter.fromIsoString(instance.getForTime());
+    if (portfolioProperties.getBookInterestInTimeSlot() == forTime.getHour())
+    {
+      final Stream<CaseEntity> activeCases = caseRepository.findByCurrentStateIn(Collections.singleton(Case.State.ACTIVE.name()));
+      activeCases.forEach(activeCase -> {
+        final ApplyInterestCommand applyInterestCommand = new ApplyInterestCommand(activeCase.getProductIdentifier(), activeCase.getIdentifier());
+        commandBus.dispatch(applyInterestCommand);
+      });
+    }
+
+    return new BeatPublishEvent(applicationName.toString(), instance.getIdentifier(), instance.getForTime());
+  }
+}
\ No newline at end of file
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 467c25a..f72ca48 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
@@ -30,9 +30,12 @@
 import io.mifos.individuallending.internal.service.*;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
 import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
 import io.mifos.portfolio.api.v1.events.EventConstants;
 import io.mifos.portfolio.service.internal.mapper.CaseMapper;
-import io.mifos.portfolio.service.internal.repository.*;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.CaseRepository;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 import io.mifos.portfolio.service.internal.util.ChargeInstance;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,8 +43,11 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.ZoneId;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -53,75 +59,91 @@
 public class IndividualLoanCommandHandler {
   private final CaseRepository caseRepository;
   private final CostComponentService costComponentService;
-  private final IndividualLoanService individualLoanService;
   private final AccountingAdapter accountingAdapter;
 
   @Autowired
   public IndividualLoanCommandHandler(
-          final CaseRepository caseRepository,
-          final CostComponentService costComponentService,
-          final IndividualLoanService individualLoanService,
-          final AccountingAdapter accountingAdapter) {
+      final CaseRepository caseRepository,
+      final CostComponentService costComponentService,
+      final AccountingAdapter accountingAdapter) {
     this.caseRepository = caseRepository;
     this.costComponentService = costComponentService;
-    this.individualLoanService = individualLoanService;
     this.accountingAdapter = accountingAdapter;
   }
 
   @Transactional
   @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
-  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE)
+  @EventEmitter(
+      selectorName = EventConstants.SELECTOR_NAME,
+      selectorValue = IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE)
   public IndividualLoanCommandEvent process(final OpenCommand command) {
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
             productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.OPEN);
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.OPEN);
 
-
-    final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
-            individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, dataContextOfAction.getCaseParameters(), BigDecimal.ZERO, Action.OPEN, today(), LocalDate.now());
-
+    final CostComponentsForRepaymentPeriod costComponents
+        = costComponentService.getCostComponentsForOpen(dataContextOfAction);
 
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
             = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
 
-    final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream().map(x -> new ChargeInstance(
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getFromAccountDesignator()),
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getToAccountDesignator()),
-            x.getValue().getAmount())).collect(Collectors.toList());
-    //TODO: Accrual
+    final List<ChargeInstance> charges = costComponents.stream()
+        .map(x -> mapCostComponentEntryToChargeInstance(Action.OPEN, x, designatorToAccountIdentifierMapper))
+        .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
             command.getCommand().getNote(),
             productIdentifier + "." + caseIdentifier + "." + Action.OPEN.name(),
             Action.OPEN.getTransactionType());
     //Only move to new state if book charges command was accepted.
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.PENDING);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.PENDING.name());
+    caseRepository.save(customerCase);
 
-    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, Action.OPEN.name());
+    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
   }
 
   @Transactional
   @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
-  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE)
+  @EventEmitter(
+      selectorName = EventConstants.SELECTOR_NAME,
+      selectorValue = IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE)
   public IndividualLoanCommandEvent process(final DenyCommand command) {
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
-    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DENY);
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
-    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
+    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+        productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DENY);
+
+    final CostComponentsForRepaymentPeriod costComponents
+        = costComponentService.getCostComponentsForDeny(dataContextOfAction);
+
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+        = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+    final List<ChargeInstance> charges = costComponents.stream()
+        .map(x -> mapCostComponentEntryToChargeInstance(Action.DENY, x, designatorToAccountIdentifierMapper))
+        .collect(Collectors.toList());
+
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.CLOSED.name());
+    caseRepository.save(customerCase);
+
+    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
   }
 
   @Transactional
   @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
-  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE)
+  @EventEmitter(
+      selectorName = EventConstants.SELECTOR_NAME,
+      selectorValue = IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE)
   public IndividualLoanCommandEvent process(final ApproveCommand command) {
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
 
     //TODO: Check for incomplete task instances.
 
@@ -139,14 +161,12 @@
             );
     caseRepository.save(dataContextOfAction.getCustomerCase());
 
-    //Charge the approval fee if applicable.
     final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
-            individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, dataContextOfAction.getCaseParameters(), BigDecimal.ZERO, Action.APPROVE, today(), LocalDate.now());
+        costComponentService.getCostComponentsForApprove(dataContextOfAction);
 
-    final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream().map(x -> new ChargeInstance(
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getFromAccountDesignator()),
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getToAccountDesignator()),
-            x.getValue().getAmount())).collect(Collectors.toList());
+    final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
+        .map(x -> mapCostComponentEntryToChargeInstance(Action.APPROVE, x, designatorToAccountIdentifierMapper))
+        .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
             command.getCommand().getNote(),
@@ -154,9 +174,11 @@
             Action.APPROVE.getTransactionType());
 
     //Only move to new state if book charges command was accepted.
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.APPROVED);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.APPROVED.name());
+    caseRepository.save(customerCase);
 
-    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, Action.APPROVE.name());
+    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
   }
 
   @Transactional
@@ -167,24 +189,19 @@
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
         productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DISBURSE);
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DISBURSE);
 
 
     final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
-        individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, dataContextOfAction.getCaseParameters(), BigDecimal.ZERO, Action.DISBURSE, today(), LocalDate.now());
-
+        costComponentService.getCostComponentsForDisburse(dataContextOfAction);
 
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
 
-    final List<ChargeInstance> charges = Stream.concat(costComponentsForRepaymentPeriod.stream().map(x -> new ChargeInstance(
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getFromAccountDesignator()),
-            designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getToAccountDesignator()),
-            x.getValue().getAmount())),
-        Stream.of(new ChargeInstance(
-            designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.PENDING_DISBURSAL),
-            designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
-                dataContextOfAction.getCaseParameters().getMaximumBalance())))
+    final BigDecimal disbursalAmount = dataContextOfAction.getCaseParameters().getMaximumBalance();
+    final List<ChargeInstance> charges = Stream.concat(
+          costComponentsForRepaymentPeriod.stream().map(x -> mapCostComponentEntryToChargeInstance(Action.DISBURSE, x, designatorToAccountIdentifierMapper)),
+          Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
         .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
@@ -192,9 +209,50 @@
         productIdentifier + "." + caseIdentifier + "." + Action.DISBURSE.name(),
         Action.DISBURSE.getTransactionType());
     //Only move to new state if book charges command was accepted.
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.ACTIVE);
+    if (Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()) != Case.State.ACTIVE) {
+      final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+      final LocalDateTime endOfTerm
+          = ScheduledActionHelpers.getRoughEndDate(LocalDate.now(ZoneId.of("UTC")), dataContextOfAction.getCaseParameters())
+          .atTime(LocalTime.MIDNIGHT);
+      customerCase.setEndOfTerm(endOfTerm);
+      customerCase.setCurrentState(Case.State.ACTIVE.name());
+      caseRepository.save(customerCase);
+    }
 
-    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, Action.DISBURSE.name());
+    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
+  }
+
+  @Transactional
+  @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+  @EventEmitter(
+      selectorName = EventConstants.SELECTOR_NAME,
+      selectorValue = IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE)
+  public IndividualLoanCommandEvent process(final ApplyInterestCommand command) {
+    final String productIdentifier = command.getProductIdentifier();
+    final String caseIdentifier = command.getCaseIdentifier();
+    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+        productIdentifier, caseIdentifier, null);
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPLY_INTEREST);
+    if (dataContextOfAction.getCustomerCase().getEndOfTerm() == null)
+      throw ServiceException.internalError(
+          "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
+
+    final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+        costComponentService.getCostComponentsForApplyInterest(dataContextOfAction);
+
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+        = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+    final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
+        .map(x -> mapCostComponentEntryToChargeInstance(Action.APPLY_INTEREST, x, designatorToAccountIdentifierMapper))
+        .collect(Collectors.toList());
+
+    accountingAdapter.bookCharges(charges,
+        "",
+        productIdentifier + "." + caseIdentifier + "." + Action.APPLY_INTEREST.name(),
+        Action.APPLY_INTEREST.getTransactionType());
+
+    return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
   }
 
   @Transactional
@@ -204,9 +262,11 @@
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.ACTIVE);
-    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.ACTIVE.name());
+    caseRepository.save(customerCase);
+    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
 
   @Transactional
@@ -216,9 +276,11 @@
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
-    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.CLOSED.name());
+    caseRepository.save(customerCase);
+    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
 
   @Transactional
@@ -228,9 +290,11 @@
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
-    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.CLOSED.name());
+    caseRepository.save(customerCase);
+    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
 
   @Transactional
@@ -240,22 +304,44 @@
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
-    checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
-    updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
-    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
-  }
-
-  private static LocalDate today() {
-    return LocalDate.now(ZoneId.of("UTC"));
-  }
-
-  private void checkActionCanBeExecuted(final Case.State state, final Action action) {
-    if (!IndividualLendingPatternFactory.getAllowedNextActionsForState(state).contains(action))
-      throw ServiceException.badRequest("Cannot call action {0} from state {1}", action.name(), state.name());
-  }
-
-  private void updateCaseState(final CaseEntity customerCase, final Case.State state) {
-    customerCase.setCurrentState(state.name());
+    IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
+    final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+    customerCase.setCurrentState(Case.State.CLOSED.name());
     caseRepository.save(customerCase);
+    return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
-}
+
+
+  private static ChargeInstance mapCostComponentEntryToChargeInstance(
+      final Action action,
+      final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
+      final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+    final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
+    if (chargeDefinition.getAccrualAccountDesignator() != null) {
+      if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
+        return new ChargeInstance(
+            designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
+            designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
+            costComponentEntry.getValue().getAmount());
+      else
+        return new ChargeInstance(
+            designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+            designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
+            costComponentEntry.getValue().getAmount());
+    }
+    else
+      return new ChargeInstance(
+          designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
+          designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+          costComponentEntry.getValue().getAmount());
+  }
+
+  private static ChargeInstance getDisbursalChargeInstance(
+      final BigDecimal amount,
+      final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+    return new ChargeInstance(
+        designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.PENDING_DISBURSAL),
+        designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
+        amount);
+  }
+}
\ No newline at end of file
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 f3b4ade..bccade4 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
@@ -18,11 +18,12 @@
 import io.mifos.core.lang.ServiceException;
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
 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.mapper.CaseParametersMapper;
 import io.mifos.individuallending.internal.repository.CaseParametersRepository;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
-import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
 import io.mifos.portfolio.api.v1.domain.CostComponent;
 import io.mifos.portfolio.service.internal.repository.CaseEntity;
 import io.mifos.portfolio.service.internal.repository.CaseRepository;
@@ -36,15 +37,15 @@
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.ZoneId;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
+import java.util.*;
+import java.util.function.BiFunction;
 
 /**
  * @author Myrle Krantz
  */
 @Service
 public class CostComponentService {
+  private static final int RUNNING_CALCULATION_PRECISION = 8;
 
   private final ProductRepository productRepository;
   private final CaseRepository caseRepository;
@@ -88,21 +89,201 @@
     return new DataContextOfAction(product, customerCase, caseParameters, oneTimeAccountAssignments);
   }
 
-  public List<CostComponent> getCostComponents(final String productIdentifier, final String caseIdentifier, final Action action) {
-    final DataContextOfAction context = checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
-    final Case.State caseState = Case.State.valueOf(context.getCustomerCase().getCurrentState());
-    final BigDecimal runningBalance;
-    if (caseState == Case.State.ACTIVE) {
-      final DesignatorToAccountIdentifierMapper mapper = new DesignatorToAccountIdentifierMapper(context);
-      final String customerLoanAccountIdentifier = mapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
-      runningBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+  public CostComponentsForRepaymentPeriod getCostComponentsForAction(
+      final Action action,
+      final DataContextOfAction dataContextOfAction) {
+    switch (action) {
+      case OPEN:
+        return getCostComponentsForOpen(dataContextOfAction);
+      case APPROVE:
+        return getCostComponentsForApprove(dataContextOfAction);
+      case DENY:
+        return getCostComponentsForDeny(dataContextOfAction);
+      case DISBURSE:
+        return getCostComponentsForDisburse(dataContextOfAction);
+      case APPLY_INTEREST:
+        return getCostComponentsForApplyInterest(dataContextOfAction);
+      case ACCEPT_PAYMENT:
+        return getCostComponentsForAcceptPayment(dataContextOfAction);
+      case CLOSE:
+        return getCostComponentsForClose(dataContextOfAction);
+      case MARK_LATE:
+        return getCostComponentsForMarkLate(dataContextOfAction);
+      case WRITE_OFF:
+        return getCostComponentsForMarkLate(dataContextOfAction);
+      case RECOVER:
+        return getCostComponentsForMarkLate(dataContextOfAction);
+      default:
+        throw ServiceException.internalError("Invalid action: ''{0}''.", action.name());
     }
-    else
-      runningBalance = BigDecimal.ZERO;
+  }
 
-    return individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, context.getCaseParameters(), runningBalance, action, LocalDate.now(ZoneId.of("UTC")), LocalDate.now(ZoneId.of("UTC")))
-            .stream()
-            .map(x -> new CostComponent(x.getKey().getIdentifier(), x.getValue().getAmount()))
-            .collect(Collectors.toList()); //TODO: initial disbursal date.
+  public CostComponentsForRepaymentPeriod getCostComponentsForOpen(final DataContextOfAction dataContextOfAction) {
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, today()));
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+            scheduledCharges,
+            caseParameters.getMaximumBalance(),
+            BigDecimal.ZERO,
+            minorCurrencyUnitDigits);
+  }
+
+  public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, today()));
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+        scheduledCharges,
+        caseParameters.getMaximumBalance(),
+        BigDecimal.ZERO,
+        minorCurrencyUnitDigits);
+  }
+
+  public CostComponentsForRepaymentPeriod getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
+    //Charge the approval fee if applicable.
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, today()));
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+            scheduledCharges,
+            caseParameters.getMaximumBalance(),
+            BigDecimal.ZERO,
+            minorCurrencyUnitDigits);
+  }
+
+  public CostComponentsForRepaymentPeriod getCostComponentsForDisburse(final DataContextOfAction dataContextOfAction) {
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DISBURSE, today()));
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+            scheduledCharges,
+            caseParameters.getMaximumBalance(),
+            BigDecimal.ZERO,
+            minorCurrencyUnitDigits);
+  }
+
+  public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(final DataContextOfAction dataContextOfAction) {
+
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+        = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+    final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getScheduledActionsForDisbursedLoan(LocalDate.now(), dataContextOfAction.getCustomerCase().getEndOfTerm().toLocalDate(), caseParameters, Action.APPLY_INTEREST);
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, currentBalance, scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+            scheduledCharges,
+            caseParameters.getMaximumBalance(),
+            currentBalance,
+            minorCurrencyUnitDigits);
+  }
+
+  private CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(final DataContextOfAction dataContextOfAction) {
+    return null;
+  }
+  private CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction) {
+    return null;
+  }
+  public CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
+    return null;
+  }
+  private CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
+    return null;
+  }
+  public CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
+    return null;
+  }
+
+  static CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
+      final Collection<ScheduledCharge> scheduledCharges,
+      final BigDecimal maximumBalance,
+      final BigDecimal runningBalance,
+      final int minorCurrencyUnitDigits) {
+    BigDecimal balanceAdjustment = BigDecimal.ZERO;
+
+    final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
+    for (final ScheduledCharge scheduledCharge : scheduledCharges)
+    {
+      final CostComponent costComponent = costComponentMap
+          .computeIfAbsent(scheduledCharge.getChargeDefinition(),
+              chargeIdentifier -> {
+                final CostComponent ret = new CostComponent();
+                ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
+                ret.setAmount(BigDecimal.ZERO);
+                return ret;
+              });
+
+      final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge)
+          .apply(maximumBalance, runningBalance)
+          .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+      if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
+        balanceAdjustment = balanceAdjustment.add(chargeAmount);
+      costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+    }
+
+    return new CostComponentsForRepaymentPeriod(
+        costComponentMap,
+        balanceAdjustment);
+  }
+
+  private static BiFunction<BigDecimal, BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
+      final ScheduledCharge scheduledCharge)
+  {
+
+    switch (scheduledCharge.getChargeDefinition().getChargeMethod())
+    {
+      case FIXED:
+        return (maximumBalance, runningBalance) -> scheduledCharge.getChargeDefinition().getAmount();
+      case PROPORTIONAL: {
+        switch (scheduledCharge.getChargeDefinition().getProportionalTo()) {
+          case ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR:
+            return (maximumBalance, runningBalance) ->
+                PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
+                    .multiply(runningBalance);
+          case ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR:
+            return (maximumBalance, runningBalance) ->
+                PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
+                    .multiply(maximumBalance);
+          default:
+//TODO: correctly implement charges which are proportionate to other charges.
+            return (maximumBalance, runningBalance) ->
+                PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
+                    .multiply(maximumBalance);
+        }
+      }
+      default:
+        return (maximumBalance, runningBalance) -> BigDecimal.ZERO;
+    }
+  }
+
+  private static boolean chargeDefinitionTouchesCustomerLoanAccount(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));
+  }
+  private static LocalDate today() {
+    return LocalDate.now(ZoneId.of("UTC"));
   }
 }
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 4167b81..e7288a2 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
@@ -15,13 +15,10 @@
  */
 package io.mifos.individuallending.internal.service;
 
-import io.mifos.core.lang.DateConverter;
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
 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.product.ChargeIdentifiers;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
 import io.mifos.portfolio.api.v1.domain.CostComponent;
@@ -38,7 +35,6 @@
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.*;
-import java.util.function.BiFunction;
 import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -57,17 +53,14 @@
   private static final int EXTRA_PRECISION = 4;
   private final ProductService productService;
   private final ChargeDefinitionService chargeDefinitionService;
-  private final ScheduledActionService scheduledActionService;
   private final PeriodChargeCalculator periodChargeCalculator;
 
   @Autowired
   public IndividualLoanService(final ProductService productService,
                                final ChargeDefinitionService chargeDefinitionService,
-                               final ScheduledActionService scheduledActionService,
                                final PeriodChargeCalculator periodChargeCalculator) {
     this.productService = productService;
     this.chargeDefinitionService = chargeDefinitionService;
-    this.scheduledActionService = scheduledActionService;
     this.periodChargeCalculator = periodChargeCalculator;
   }
 
@@ -81,7 +74,7 @@
             .orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
     final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
 
-    final List<ScheduledAction> scheduledActions = scheduledActionService.getHypotheticalScheduledActions(initialDisbursalDate, caseParameters);
+    final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, caseParameters);
 
     final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, caseParameters.getMaximumBalance(), scheduledActions);
 
@@ -113,34 +106,15 @@
     return ret;
   }
 
-  public CostComponentsForRepaymentPeriod getCostComponentsForRepaymentPeriod(final String productIdentifier,
-                                                                              final CaseParameters caseParameters,
-                                                                              final BigDecimal runningBalance,
-                                                                              final Action action,
-                                                                              final LocalDate initialDisbursalDate,
-                                                                              final LocalDate forDate) {
-    final Product product = productService.findByIdentifier(productIdentifier)
-            .orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
-    final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
-    final List<ScheduledAction> scheduledActions = scheduledActionService.getScheduledActions(initialDisbursalDate, caseParameters, action, forDate);
-    final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, runningBalance, scheduledActions);
-
-    return getCostComponentsForScheduledCharges(
-            scheduledCharges,
-            caseParameters.getMaximumBalance(),
-            runningBalance,
-            minorCurrencyUnitDigits);
-  }
-
   private static ChargeName chargeNameFromChargeDefinition(final ScheduledCharge scheduledCharge) {
     return new ChargeName(scheduledCharge.getChargeDefinition().getIdentifier(), scheduledCharge.getChargeDefinition().getName());
   }
 
-  private List<ScheduledCharge> getScheduledCharges(
-          final String productIdentifier,
-          final int minorCurrencyUnitDigits,
-          final BigDecimal initialBalance,
-          final @Nonnull List<ScheduledAction> scheduledActions) {
+  List<ScheduledCharge> getScheduledCharges(
+      final String productIdentifier,
+      final int minorCurrencyUnitDigits,
+      final BigDecimal initialBalance,
+      final @Nonnull List<ScheduledAction> scheduledActions) {
     final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
             = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
 
@@ -199,7 +173,13 @@
           final List<ScheduledCharge> scheduledCharges) {
     final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
             = scheduledCharges.stream()
-            .collect(Collectors.groupingBy(x -> x.getScheduledAction().repaymentPeriod,
+            .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()),
@@ -207,8 +187,7 @@
                                     (left, right) -> { left.addAll(right); return left; }))));
 
     final SortedSet<Period> sortedRepaymentPeriods
-            = scheduledCharges.stream()
-            .map(x -> x.getScheduledAction().repaymentPeriod)
+            = orderedScheduledChargesGroupedByPeriod.keySet().stream()
             .collect(Collector.of(TreeSet::new, TreeSet::add, (left, right) -> { left.addAll(right); return left; }));
 
     BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
@@ -217,11 +196,11 @@
     {
       final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
       final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
-              getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
+              CostComponentService.getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
 
       final PlannedPayment plannedPayment = new PlannedPayment();
-      plannedPayment.setCostComponents(costComponentsForRepaymentPeriod.costComponents.values().stream().collect(Collectors.toList()));
-      plannedPayment.setDate(DateConverter.toIsoString(repaymentPeriod.getEndDate()));
+      plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.costComponents.values()));
+      plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
       balance = balance.add(costComponentsForRepaymentPeriod.balanceAdjustment);
       plannedPayment.setRemainingPrincipal(balance);
       plannedPayments.add(plannedPayment);
@@ -239,67 +218,6 @@
     return plannedPayments;
   }
 
-  static private CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
-          final Collection<ScheduledCharge> scheduledCharges,
-          final BigDecimal maximumBalance,
-          final BigDecimal runningBalance,
-          final int minorCurrencyUnitDigits) {
-    BigDecimal balanceAdjustment = BigDecimal.ZERO;
-
-    final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
-    for (final ScheduledCharge scheduledCharge : scheduledCharges)
-    {
-      final CostComponent costComponent = costComponentMap
-              .computeIfAbsent(scheduledCharge.getChargeDefinition(),
-                      chargeIdentifier -> {
-                        final CostComponent ret = new CostComponent();
-                        ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
-                        ret.setAmount(BigDecimal.ZERO);
-                        return ret;
-                      });
-
-      final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge, 8)
-              .apply(maximumBalance, runningBalance)
-              .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
-      if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
-        balanceAdjustment = balanceAdjustment.add(chargeAmount);
-      costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
-    }
-
-    return new CostComponentsForRepaymentPeriod(
-            costComponentMap,
-            balanceAdjustment);
-  }
-
-  private static boolean chargeDefinitionTouchesCustomerLoanAccount(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));
-  }
-
-  private static BiFunction<BigDecimal, BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
-          final ScheduledCharge scheduledCharge,
-          final int precision)
-  {
-
-    switch (scheduledCharge.getChargeDefinition().getChargeMethod())
-    {
-      case FIXED:
-        return (maximumBalance, runningBalance) -> scheduledCharge.getChargeDefinition().getAmount();
-      case PROPORTIONAL: {
-        if (scheduledCharge.getChargeDefinition().getProportionalTo().equals(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR))
-          return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(runningBalance);
-        else if (scheduledCharge.getChargeDefinition().getProportionalTo().equals(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR))
-          return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(maximumBalance);
-        else //TODO: correctly implement charges which are proportionate to other charges.
-          return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(maximumBalance);
-      }
-      default:
-        return (maximumBalance, runningBalance) -> BigDecimal.ZERO;
-    }
-  }
-
   private BigDecimal loanPaymentInContextOfAccruedInterest(
           final BigDecimal initialBalance,
           final int periodCount,
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 aa4f170..ecd04f2 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
@@ -15,6 +15,8 @@
  */
 package io.mifos.individuallending.internal.service;
 
+import io.mifos.core.lang.DateConverter;
+
 import javax.annotation.Nonnull;
 import java.time.Duration;
 import java.time.LocalDate;
@@ -46,6 +48,10 @@
     return endDate;
   }
 
+  String getEndDateAsString() {
+    return endDate == null ? null : DateConverter.toIsoString(endDate);
+  }
+
   Duration getDuration() {
     long days = beginDate.until(endDate, ChronoUnit.DAYS);
     return ChronoUnit.DAYS.getDuration().multipliedBy(days);
@@ -71,13 +77,26 @@
 
   @Override
   public int compareTo(@Nonnull Period o) {
-    int comparison = endDate.compareTo(o.endDate);
+    final int comparison = compareNullableDates(endDate, o.endDate);
+
     if (comparison == 0)
-      return beginDate.compareTo(o.beginDate);
+      return compareNullableDates(beginDate, o.beginDate);
     else
       return comparison;
   }
 
+  @SuppressWarnings("ConstantConditions")
+  private static int compareNullableDates(final LocalDate x, final LocalDate y) {
+    if ((x == null) && (y == null))
+      return 0;
+    else if ((x == null) && (y != null))
+      return -1;
+    else if ((x != null) && (y == null))
+      return 1;
+    else
+      return x.compareTo(y);
+  }
+
   @Override
   public String toString() {
     return "Period{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
index 7f5b57a..30c77fe 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
@@ -18,17 +18,18 @@
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.time.LocalDate;
 import java.util.Objects;
 
 /**
  * @author Myrle Krantz
  */
-class ScheduledAction {
+public class ScheduledAction {
   final Action action;
   final LocalDate when;
-  final Period actionPeriod;
-  final Period repaymentPeriod;
+  final @Nullable Period actionPeriod;
+  final @Nullable Period repaymentPeriod;
 
   ScheduledAction(@Nonnull final Action action,
                   @Nonnull final LocalDate when,
@@ -40,6 +41,14 @@
     this.repaymentPeriod = repaymentPeriod;
   }
 
+  ScheduledAction(@Nonnull final Action action,
+                  @Nonnull final LocalDate when) {
+    this.action = action;
+    this.when = when;
+    this.actionPeriod = null;
+    this.repaymentPeriod = null;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
similarity index 64%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
index 87a126e..80dcf48 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
@@ -18,12 +18,12 @@
 import io.mifos.portfolio.api.v1.domain.PaymentCycle;
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import org.springframework.stereotype.Service;
 
 import javax.annotation.Nonnull;
 import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.YearMonth;
+import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.SortedSet;
@@ -35,36 +35,55 @@
  * @author Myrle Krantz
  */
 @SuppressWarnings("WeakerAccess")
-@Service
-public class ScheduledActionService {
-
-  List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
-                                                                final @Nonnull CaseParameters caseParameters)
-  {
-    return getHypotheticalScheduledActionsHelper(initialDisbursalDate, caseParameters).collect(Collectors.toList());
+public class ScheduledActionHelpers {
+  public static boolean actionHasNoActionPeriod(final Action action) {
+    return preDisbursalActions().anyMatch(x -> action == x);
   }
 
-  private Stream<ScheduledAction> getHypotheticalScheduledActionsHelper(final @Nonnull LocalDate initialDisbursalDate,
+  private static Stream<Action> preDisbursalActions() {
+    return Stream.of(Action.OPEN, Action.APPROVE, Action.DISBURSE);
+  }
+
+  public static List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
                                                         final @Nonnull CaseParameters caseParameters)
   {
-    //'Rough' end date, because if the repayment period takes the last period after that end date, then the repayment
-    // period will 'win'.
-    final LocalDate roughEndDate = getEndDate(caseParameters, initialDisbursalDate);
-
-    final SortedSet<Period> repaymentPeriods = generateRepaymentPeriods(initialDisbursalDate, roughEndDate, caseParameters);
-    final Period firstPeriod = repaymentPeriods.first();
-    final Period lastPeriod = repaymentPeriods.last();
-
-    return Stream.concat(Stream.of(
-        new ScheduledAction(Action.OPEN, initialDisbursalDate, firstPeriod, firstPeriod),
-        new ScheduledAction(Action.APPROVE, initialDisbursalDate, firstPeriod, firstPeriod),
-        new ScheduledAction(Action.DISBURSE, initialDisbursalDate, firstPeriod, firstPeriod)),
-        Stream.concat(repaymentPeriods.stream().flatMap(this::generateScheduledActionsForRepaymentPeriod),
-            Stream.of(new ScheduledAction(Action.CLOSE, lastPeriod.getEndDate(), lastPeriod, lastPeriod))));
+    final LocalDate endOfTerm = getRoughEndDate(initialDisbursalDate, caseParameters);
+    return Stream.concat(preDisbursalActions().map(action -> new ScheduledAction(action, initialDisbursalDate)),
+        getHypotheticalScheduledActionsForDisbursedLoan(initialDisbursalDate, endOfTerm, caseParameters))
+        .collect(Collectors.toList());
   }
 
-  private LocalDate getEndDate(final @Nonnull CaseParameters caseParameters,
-                               final @Nonnull LocalDate initialDisbursalDate) {
+  public static List<ScheduledAction> getScheduledActionsForDisbursedLoan(final @Nonnull LocalDate forDate,
+                                                                   final @Nonnull LocalDate endOfTerm,
+                                                                   final @Nonnull CaseParameters caseParameters,
+                                                                   final @Nonnull Action action) {
+    if (preDisbursalActions().anyMatch(x -> action == x))
+      throw new IllegalStateException("Should not be calling getScheduledActionsForDisbursedLoan with an action which occurs before disbursement.");
+
+    final LocalDate today = LocalDate.now(ZoneId.of("UTC"));
+    return getHypotheticalScheduledActionsForDisbursedLoan(today, endOfTerm, caseParameters)
+        .filter(x -> x.action.equals(action))
+        .filter(x -> x.actionPeriod != null && x.actionPeriod.containsDate(forDate))
+        .collect(Collectors.toList());
+  }
+
+  private static Stream<ScheduledAction> getHypotheticalScheduledActionsForDisbursedLoan(
+      final @Nonnull LocalDate initialDisbursalDate,
+      final @Nonnull LocalDate endOfTerm,
+      final @Nonnull CaseParameters caseParameters)
+  {
+    final SortedSet<Period> repaymentPeriods = generateRepaymentPeriods(initialDisbursalDate, endOfTerm, caseParameters);
+    final Period lastPeriod = repaymentPeriods.last();
+
+    return Stream.concat(repaymentPeriods.stream().flatMap(ScheduledActionHelpers::generateScheduledActionsForRepaymentPeriod),
+        Stream.of(new ScheduledAction(Action.CLOSE, lastPeriod.getEndDate(), lastPeriod, lastPeriod)));
+  }
+
+  /** 'Rough' end date, because if the repayment period takes the last period after that end date, then the repayment
+   period will 'win'.*/
+
+  public static LocalDate getRoughEndDate(final @Nonnull LocalDate initialDisbursalDate,
+                                          final @Nonnull CaseParameters caseParameters) {
     final Integer maximumTermSize = caseParameters.getTermRange().getMaximum();
     final ChronoUnit termUnit = caseParameters.getTermRange().getTemporalUnit();
 
@@ -73,22 +92,22 @@
             termUnit);
   }
 
-  private Stream<ScheduledAction> generateScheduledActionsForRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
+  private static Stream<ScheduledAction> generateScheduledActionsForRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
     return Stream.concat(generateScheduledInterestPaymentsForRepaymentPeriod(repaymentPeriod),
             Stream.of(new ScheduledAction(Action.ACCEPT_PAYMENT, repaymentPeriod.getEndDate(), repaymentPeriod, repaymentPeriod)));
   }
 
-  private Stream<ScheduledAction> generateScheduledInterestPaymentsForRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
+  private static Stream<ScheduledAction> generateScheduledInterestPaymentsForRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
     return getInterestDayInRepaymentPeriod(repaymentPeriod).map(x ->
             new ScheduledAction(Action.APPLY_INTEREST, x, new Period(x.minus(1, ChronoUnit.DAYS), x), repaymentPeriod));
   }
 
-  private Stream<LocalDate> getInterestDayInRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
+  private static Stream<LocalDate> getInterestDayInRepaymentPeriod(final @Nonnull Period repaymentPeriod) {
     return Stream.iterate(repaymentPeriod.getBeginDate().plusDays(1), date -> date.plusDays(1))
             .limit(ChronoUnit.DAYS.between(repaymentPeriod.getBeginDate(), repaymentPeriod.getEndDate()));
   }
 
-  private SortedSet<Period> generateRepaymentPeriods(
+  private static SortedSet<Period> generateRepaymentPeriods(
           final LocalDate initialDisbursalDate,
           final LocalDate endDate,
           final CaseParameters caseParameters) {
@@ -108,7 +127,7 @@
     return ret;
   }
 
-  private LocalDate generateNextPaymentDate(final CaseParameters caseParameters, final LocalDate lastPaymentDate) {
+  private static LocalDate generateNextPaymentDate(final CaseParameters caseParameters, final LocalDate lastPaymentDate) {
     final PaymentCycle paymentCycle = caseParameters.getPaymentCycle();
 
     final ChronoUnit maximumSpecifiedAlignmentChronoUnit =
@@ -131,13 +150,13 @@
     return alignPaymentDate(orientedPaymentDate, maximumAlignmentChronoUnit, paymentCycle);
   }
 
-  private LocalDate incrementPaymentDate(LocalDate paymentDate, PaymentCycle paymentCycle) {
+  private static LocalDate incrementPaymentDate(LocalDate paymentDate, PaymentCycle paymentCycle) {
     return paymentDate.plus(
             paymentCycle.getPeriod(),
             paymentCycle.getTemporalUnit());
   }
 
-  private LocalDate orientPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumSpecifiedAlignmentChronoUnit, PaymentCycle paymentCycle) {
+  private static LocalDate orientPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumSpecifiedAlignmentChronoUnit, PaymentCycle paymentCycle) {
     if (maximumSpecifiedAlignmentChronoUnit == ChronoUnit.HOURS)
       return paymentDate; //No need to orient at all since no alignment is specified.
 
@@ -155,28 +174,28 @@
     }
   }
 
-  private @Nonnull ChronoUnit min(@Nonnull final ChronoUnit a, @Nonnull final ChronoUnit b) {
+  private static @Nonnull ChronoUnit min(@Nonnull final ChronoUnit a, @Nonnull final ChronoUnit b) {
     if (a.getDuration().compareTo(b.getDuration()) < 0)
       return a;
     else
       return b;
   }
 
-  private LocalDate orientInYear(final LocalDate paymentDate) {
+  private static LocalDate orientInYear(final LocalDate paymentDate) {
     return LocalDate.of(paymentDate.getYear(), 1, 1);
   }
 
-  private LocalDate orientInMonth(final LocalDate paymentDate) {
+  private static LocalDate orientInMonth(final LocalDate paymentDate) {
     return LocalDate.of(paymentDate.getYear(), paymentDate.getMonth(), 1);
   }
 
-  private LocalDate orientInWeek(final LocalDate paymentDate) {
+  private static LocalDate orientInWeek(final LocalDate paymentDate) {
     final DayOfWeek dayOfWeek = paymentDate.getDayOfWeek();
     final int dayOfWeekIndex = dayOfWeek.getValue() - 1;
     return paymentDate.minusDays(dayOfWeekIndex);
   }
 
-  private LocalDate alignPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumAlignmentChronoUnit, final PaymentCycle paymentCycle) {
+  private static LocalDate alignPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumAlignmentChronoUnit, final PaymentCycle paymentCycle) {
     LocalDate ret = paymentDate;
     switch (maximumAlignmentChronoUnit)
     {
@@ -192,7 +211,7 @@
     }
   }
 
-  private LocalDate alignInMonths(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
+  private static LocalDate alignInMonths(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
     final Integer alignmentMonth = paymentCycle.getAlignmentMonth();
     if (alignmentMonth == null)
       return paymentDate;
@@ -200,7 +219,7 @@
     return paymentDate.plusMonths(alignmentMonth);
   }
 
-  private LocalDate alignInWeeks(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
+  private static LocalDate alignInWeeks(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
     final Integer alignmentWeek = paymentCycle.getAlignmentWeek();
     if (alignmentWeek == null)
       return paymentDate;
@@ -220,7 +239,7 @@
     throw new IllegalStateException("PaymentCycle.alignmentWeek should only ever be 0, 1, 2, or -1.");
   }
 
-  private LocalDate alignInDays(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
+  static private LocalDate alignInDays(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
     final Integer alignmentDay = paymentCycle.getAlignmentDay();
     if (alignmentDay == null)
       return paymentDate;
@@ -231,7 +250,7 @@
       return alignInDaysOfMonth(paymentDate, alignmentDay);
   }
 
-  private LocalDate alignInDaysOfWeek(final LocalDate paymentDate, final Integer alignmentDay) {
+  static private LocalDate alignInDaysOfWeek(final LocalDate paymentDate, final Integer alignmentDay) {
     final int dayOfWeek = paymentDate.getDayOfWeek().getValue()-1;
 
     if (dayOfWeek < alignmentDay)
@@ -242,18 +261,8 @@
       return paymentDate;
   }
 
-  private LocalDate alignInDaysOfMonth(final LocalDate paymentDate, final Integer alignmentDay) {
+  private static LocalDate alignInDaysOfMonth(final LocalDate paymentDate, final Integer alignmentDay) {
     final int maxDay = YearMonth.of(paymentDate.getYear(), paymentDate.getMonth()).lengthOfMonth()-1;
     return paymentDate.plusDays(Math.min(maxDay, alignmentDay));
   }
-
-  public List<ScheduledAction> getScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
-                                                   final CaseParameters caseParameters,
-                                                   final Action action,
-                                                   final LocalDate time) {
-    return getHypotheticalScheduledActionsHelper(initialDisbursalDate, caseParameters)
-            .filter(x -> x.actionPeriod.containsDate(time))
-            .filter(x -> x.action.equals(action))
-            .collect(Collectors.toList());
-  }
 }
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
index 15cbf30..60fb56c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
@@ -22,7 +22,7 @@
 /**
  * @author Myrle Krantz
  */
-class ScheduledCharge {
+public class ScheduledCharge {
   private final ScheduledAction scheduledAction;
   private final ChargeDefinition chargeDefinition;
 
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/BeatPublishListenerRestController.java b/service/src/main/java/io/mifos/individuallending/rest/BeatPublishListenerRestController.java
similarity index 97%
rename from service/src/main/java/io/mifos/portfolio/service/rest/BeatPublishListenerRestController.java
rename to service/src/main/java/io/mifos/individuallending/rest/BeatPublishListenerRestController.java
index ef7bda8..4da6b4b 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/BeatPublishListenerRestController.java
+++ b/service/src/main/java/io/mifos/individuallending/rest/BeatPublishListenerRestController.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.portfolio.service.rest;
+package io.mifos.individuallending.rest;
 
 import io.mifos.anubis.annotation.AcceptedTokenType;
 import io.mifos.anubis.annotation.Permittable;
diff --git a/service/src/main/java/io/mifos/portfolio/service/config/PortfolioProperties.java b/service/src/main/java/io/mifos/portfolio/service/config/PortfolioProperties.java
index 98936f6..25cf294 100644
--- a/service/src/main/java/io/mifos/portfolio/service/config/PortfolioProperties.java
+++ b/service/src/main/java/io/mifos/portfolio/service/config/PortfolioProperties.java
@@ -16,6 +16,7 @@
 package io.mifos.portfolio.service.config;
 
 import io.mifos.core.lang.validation.constraints.ValidIdentifier;
+import org.hibernate.validator.constraints.Range;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.annotation.Validated;
@@ -31,6 +32,9 @@
   @ValidIdentifier
   private String bookInterestAsUser;
 
+  @Range(min=0, max=23)
+  private int bookInterestInTimeSlot = 0;
+
   public PortfolioProperties() {
   }
 
@@ -41,4 +45,12 @@
   public void setBookInterestAsUser(String bookInterestAsUser) {
     this.bookInterestAsUser = bookInterestAsUser;
   }
+
+  public int getBookInterestInTimeSlot() {
+    return bookInterestInTimeSlot;
+  }
+
+  public void setBookInterestInTimeSlot(int bookInterestInTimeSlot) {
+    this.bookInterestInTimeSlot = bookInterestInTimeSlot;
+  }
 }
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BeatPublishCommandHandler.java
deleted file mode 100644
index d879094..0000000
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BeatPublishCommandHandler.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2017 The Mifos Initiative.
- *
- * 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.command.handler;
-
-import io.mifos.core.command.annotation.Aggregate;
-import io.mifos.core.command.annotation.CommandHandler;
-import io.mifos.core.command.annotation.CommandLogLevel;
-import io.mifos.core.command.annotation.EventEmitter;
-import io.mifos.core.lang.ApplicationName;
-import io.mifos.rhythm.spi.v1.domain.BeatPublish;
-import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
-import io.mifos.rhythm.spi.v1.events.EventConstants;
-import io.mifos.portfolio.service.internal.command.CreateBeatPublishCommand;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * @author Myrle Krantz
- */
-@SuppressWarnings("unused")
-@Aggregate
-public class BeatPublishCommandHandler {
-
-  private final ApplicationName applicationName;
-
-  @Autowired
-  public BeatPublishCommandHandler(final ApplicationName applicationName) {
-    this.applicationName = applicationName;
-  }
-
-  @Transactional
-  @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
-  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_PUBLISHEDBEAT)
-  public BeatPublishEvent process(final CreateBeatPublishCommand createBeatPublishCommand) {
-    final BeatPublish instance = createBeatPublishCommand.getInstance();
-    return new BeatPublishEvent(applicationName.toString(), instance.getIdentifier(), instance.getForTime());
-  }
-}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
index ab3eff1..1ff341c 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
@@ -23,6 +23,7 @@
 import io.mifos.portfolio.api.v1.events.EventConstants;
 import io.mifos.portfolio.service.internal.command.InitializeServiceCommand;
 import io.mifos.portfolio.service.internal.util.RhythmAdapter;
+import org.flywaydb.core.Flyway;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import javax.sql.DataSource;
@@ -51,7 +52,9 @@
   @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
   @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.INITIALIZE)
   public String initialize(final InitializeServiceCommand initializeServiceCommand) {
-    this.flywayFactoryBean.create(this.dataSource).migrate();
+    final Flyway flyway = this.flywayFactoryBean.create(this.dataSource);
+    System.out.println("Baseline version: " + flyway.getBaselineVersion());
+    flyway.migrate();
     rhythmAdapter.request24Beats();
     return EventConstants.INITIALIZE;
   }
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
index 27389f1..4e470b3 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
@@ -22,6 +22,7 @@
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 
+import java.math.BigDecimal;
 import java.time.Clock;
 import java.time.LocalDateTime;
 import java.util.List;
@@ -41,7 +42,7 @@
             new TermRange(productEntity.getTermRangeTemporalUnit(),
                     productEntity.getTermRangeMaximum()));
     product.setBalanceRange(
-            new BalanceRange(productEntity.getBalanceRangeMinimum(), productEntity.getBalanceRangeMaximum()));
+            new BalanceRange(productEntity.getBalanceRangeMinimum().setScale(productEntity.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_HALF_EVEN), productEntity.getBalanceRangeMaximum().setScale(productEntity.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_HALF_EVEN)));
     product.setInterestRange(
             new InterestRange(productEntity.getInterestRangeMinimum(), productEntity.getInterestRangeMaximum()));
     product.setInterestBasis(productEntity.getInterestBasis());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
index 6f71a1f..f86092f 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseEntity.java
@@ -17,6 +17,7 @@
 
 import io.mifos.core.mariadb.util.LocalDateTimeConverter;
 
+import javax.annotation.Nullable;
 import javax.persistence.*;
 import java.time.LocalDateTime;
 import java.util.Set;
@@ -33,18 +34,22 @@
   @Column(name = "id")
   private Long id;
 
-  @Column(name = "identifier")
+  @Column(name = "identifier", nullable = false)
   private String identifier;
 
-  @Column(name = "product_identifier")
+  @Column(name = "product_identifier", nullable = false)
   private String productIdentifier;
 
   @OneToMany(targetEntity = CaseAccountAssignmentEntity.class, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "caseEntity")
   private Set<CaseAccountAssignmentEntity> accountAssignments;
 
-  @Column(name = "current_state")
+  @Column(name = "current_state", nullable = false)
   private String currentState;
 
+  @Column(name = "end_of_term")
+  @Convert(converter = LocalDateTimeConverter.class)
+  @Nullable private LocalDateTime endOfTerm;
+
   @Column(name = "created_on")
   @Convert(converter = LocalDateTimeConverter.class)
   private LocalDateTime createdOn;
@@ -102,6 +107,14 @@
     this.currentState = currentState;
   }
 
+  public LocalDateTime getEndOfTerm() {
+    return endOfTerm;
+  }
+
+  public void setEndOfTerm(LocalDateTime endOfTerm) {
+    this.endOfTerm = endOfTerm;
+  }
+
   public LocalDateTime getCreatedOn() {
     return createdOn;
   }
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseRepository.java
index b83c890..2d439f6 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseRepository.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseRepository.java
@@ -24,6 +24,7 @@
 
 import java.util.Collection;
 import java.util.Optional;
+import java.util.stream.Stream;
 
 /**
  * @author Myrle Krantz
@@ -37,4 +38,6 @@
   @Query("SELECT COUNT(t) > 0  FROM CaseEntity t WHERE t.productIdentifier = :productIdentifier")
   boolean existsByProductIdentifier(@Param("productIdentifier") String productIdentifier);
 
+  Stream<CaseEntity> findByCurrentStateIn(Collection<String> currentStates);
+
 }
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 f88a277..755640f 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
@@ -21,6 +21,7 @@
 import io.mifos.accounting.api.v1.domain.*;
 import io.mifos.core.api.util.UserContextHolder;
 import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.ServiceException;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
 import org.apache.commons.lang.RandomStringUtils;
@@ -29,10 +30,7 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -102,8 +100,13 @@
   }
 
   public BigDecimal getCurrentBalance(final String accountIdentifier) {
-    final Account account = ledgerManager.findAccount(accountIdentifier);
-    return BigDecimal.valueOf(account.getBalance());
+    try {
+      final Account account = ledgerManager.findAccount(accountIdentifier);
+      return BigDecimal.valueOf(account.getBalance());
+    }
+    catch (final AccountNotFoundException e) {
+     throw ServiceException.internalError("Could not found the account with the identifier ''{0}''", accountIdentifier);
+    }
   }
 
   public String createAccountForLedgerAssignment(final String customerIdentifier, final AccountAssignment ledgerAssignment) {
@@ -138,7 +141,7 @@
   public static Set<String> getRequiredAccountDesignators(final Collection<ChargeDefinition> chargeDefinitionEntities) {
     return chargeDefinitionEntities.stream()
             .flatMap(AccountingAdapter::getAutomaticActionAccountDesignators)
-            .filter(x -> x != null)
+            .filter(Objects::nonNull)
             .collect(Collectors.toSet());
   }
 
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
index aefab79..d460ea7 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
@@ -112,8 +112,8 @@
   @RequestMapping(
           value = "{chargedefinitionidentifier}",
           method = RequestMethod.PUT,
-          consumes = MediaType.APPLICATION_JSON_VALUE,
-          produces = MediaType.ALL_VALUE
+          consumes = MediaType.ALL_VALUE,
+          produces = MediaType.APPLICATION_JSON_VALUE
   )
   public ResponseEntity<Void> changeChargeDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
index a15f8b7..4493358 100644
--- a/service/src/main/resources/application.yml
+++ b/service/src/main/resources/application.yml
@@ -83,4 +83,5 @@
     modulus: 21188023007955682867939457181271038457216099278949187456460742046123672432355777599460689470319454021384777684967830053993002724303461144745107517305075315187397862430851722919529943465029389248042840364475999768651348557757734298942211509744303551097953258597691851996692366468761965138767429272032120029271744611798874201312092155969603381492096789028306859853929900848124928201000469425135976322303229632628092728624143573273277870884919055453251617011673264035045823652246768583219018126865521694880333238485410601803458379987829318615730229086183405850999386270584135805252231189505197494383178133769189765423639
 
 portfolio:
-  bookInterestAsUser: interest_user
\ No newline at end of file
+  bookInterestAsUser: interest_user
+  bookInterestInTimeSlot: 0
\ No newline at end of file
diff --git a/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql b/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql
index f094533..0bcfebc 100644
--- a/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql
+++ b/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql
@@ -16,4 +16,6 @@
 
 # noinspection SqlNoDataSourceInspectionForFile
 
-ALTER TABLE bastet_p_chrg_defs ADD COLUMN proportional_to VARCHAR(32) NULL;
\ No newline at end of file
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN proportional_to VARCHAR(32) NULL DEFAULT NULL;
+
+ALTER TABLE bastet_cases ADD COLUMN end_of_term TIMESTAMP(3) NULL DEFAULT NULL;
\ No newline at end of file
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 70b5b34..de3c107 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
@@ -251,7 +251,7 @@
             .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, PAYMENT_ID)));
+            .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, LOAN_FUNDS_ALLOCATION_ID, RETURN_DISBURSEMENT_ID, LOAN_ORIGINATION_FEE_ID, INTEREST_ID, DISBURSEMENT_FEE_ID, PAYMENT_ID)));
   }
 
   private static List<ChargeDefinition> getInterestChargeDefinition(final double amount, final ChronoUnit forCycleSizeUnit) {
@@ -317,7 +317,7 @@
     Mockito.doReturn(Optional.of(product)).when(productServiceMock).findByIdentifier(testCase.productIdentifier);
     Mockito.doReturn(testCase.chargeDefinitionsMappedByAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
 
-    testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new ScheduledActionService(), new PeriodChargeCalculator());
+    testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new PeriodChargeCalculator());
   }
 
   @Test
@@ -357,9 +357,13 @@
     });
 
     //All customer payments should be within one percent of each other.
-    final Set<BigDecimal> customerPayments = allPlannedPayments.stream().map(this::getCustomerPayment).collect(Collectors.toSet());
-    final Optional<BigDecimal> maxPayment = customerPayments.stream().collect(Collectors.maxBy(BigDecimal::compareTo));
-    final Optional<BigDecimal> minPayment = customerPayments.stream().collect(Collectors.minBy(BigDecimal::compareTo));
+    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());
     Assert.assertTrue(minPayment.isPresent());
     final double percentDifference = percentDifference(maxPayment.get(), minPayment.get());
@@ -377,36 +381,17 @@
     Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
   }
 
-  @Test
-  public void getCostComponentsForRepaymentPeriod() {
-    testCase.chargeInstancesForActions.entrySet().forEach(entry ->
-            Assert.assertEquals(
-                    entry.getValue().stream().collect(Collectors.toSet()),
-                    testSubject.getCostComponentsForRepaymentPeriod(
-                            testCase.productIdentifier,
-                            testCase.caseParameters,
-                            testCase.caseParameters.getMaximumBalance(),
-                            entry.getKey().getAction(),
-                            testCase.initialDisbursementDate, entry.getKey().getLocalDate())
-                    .stream()
-                    .map(x -> new ChargeInstance(x.getKey().getFromAccountDesignator(), x.getKey().getToAccountDesignator(), x.getValue().getAmount()))
-                    .collect(Collectors.toSet())
-            ));
-  }
-
   private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {
     final BigDecimal difference = maxPayment.subtract(minPayment);
     final BigDecimal percentDifference = difference.divide(maxPayment, 4, BigDecimal.ROUND_UP);
     return percentDifference.doubleValue();
   }
 
-  private BigDecimal getCustomerPayment(final PlannedPayment plannedPayment) {
+  private Optional<BigDecimal> getCustomerRepayment(final PlannedPayment plannedPayment) {
     final Optional<CostComponent> ret = plannedPayment.getCostComponents().stream()
             .filter(y -> y.getChargeIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
             .findAny();
 
-    Assert.assertTrue(ret.isPresent());
-
-    return ret.get().getAmount().abs();
+    return ret.map(x -> x.getAmount().abs());
   }
 }
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java
similarity index 94%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java
index 1cc2fb6..9c3b265 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java
@@ -35,7 +35,7 @@
  * @author Myrle Krantz
  */
 @RunWith(Parameterized.class)
-public class ScheduledActionServiceTest {
+public class ScheduledActionHelperTest {
   private static class TestCase
   {
     final String description;
@@ -355,17 +355,17 @@
 
   private final TestCase testCase;
 
-  public ScheduledActionServiceTest(final TestCase testCase)
+  public ScheduledActionHelperTest(final TestCase testCase)
   {
     this.testCase = testCase;
   }
 
   @Test
   public void getScheduledActions() throws Exception {
-    final ScheduledActionService testSubject = new ScheduledActionService();
-    final List<ScheduledAction> result = testSubject.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
+    final List<ScheduledAction> result = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
 
-    Assert.assertTrue(testCase.description, result.containsAll(testCase.expectedResultContents));
+    Assert.assertTrue("Case " + testCase.description + " should contain " + testCase.expectedResultContents,
+        result.containsAll(testCase.expectedResultContents));
     result.forEach(x -> {
       Assert.assertTrue(x.toString(), testCase.earliestActionDate.isBefore(x.when) || testCase.earliestActionDate.isEqual(x.when));
       Assert.assertTrue(x.toString(), testCase.latestActionDate.isAfter(x.when) || testCase.latestActionDate.isEqual(x.when));
@@ -374,15 +374,19 @@
     Assert.assertEquals(testCase.expectedInterestCount, countActionsByType(result, Action.APPLY_INTEREST));
     Assert.assertEquals(1, countActionsByType(result, Action.APPROVE));
     Assert.assertEquals(1, countActionsByType(result, Action.CLOSE));
-    result.forEach(x -> Assert.assertNotNull(x.actionPeriod));
-    result.forEach(x -> Assert.assertNotNull(x.repaymentPeriod));
+    result.stream().filter(scheduledAction -> !ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
+        .forEach(scheduledAction -> {
+          Assert.assertNotNull("The action period of " + scheduledAction.toString() + " should not be null.",
+              scheduledAction.actionPeriod);
+          Assert.assertNotNull("The repayment period of " + scheduledAction.toString() + " should not be null.",
+              scheduledAction.repaymentPeriod);
+        });
     Assert.assertTrue(noDuplicatesInResult(result));
     Assert.assertTrue(maximumOneInterestPerDay(result));
   }
 
   private long countActionsByType(final List<ScheduledAction> scheduledActions, final Action actionToCount) {
-    return scheduledActions.stream().filter(x -> x.action == actionToCount)
-            .collect(Collectors.counting());
+    return scheduledActions.stream().filter(x -> x.action == actionToCount).count();
   }
 
   private boolean maximumOneInterestPerDay(final List<ScheduledAction> result) {