Merge pull request #32 from myrle-krantz/develop

straightening up task definitions in preparation for attacking task instances.
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 3814efe..35a7701 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
@@ -138,6 +138,7 @@
           produces = MediaType.APPLICATION_JSON_VALUE,
           consumes = MediaType.APPLICATION_JSON_VALUE
   )
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
   void createTaskDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
           final TaskDefinition taskDefinition);
@@ -153,16 +154,25 @@
           @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier);
 
   @RequestMapping(
-          value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
-          method = RequestMethod.PUT,
-          produces = MediaType.ALL_VALUE,
-          consumes = MediaType.APPLICATION_JSON_VALUE
-  )
+      value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
+      method = RequestMethod.PUT,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE)
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
   void changeTaskDefinition(
-          @PathVariable("productidentifier") final String productIdentifier,
-          @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
-          final TaskDefinition taskDefinition);
+      @PathVariable("productidentifier") final String productIdentifier,
+      @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
+      final TaskDefinition taskDefinition);
 
+  @RequestMapping(
+      value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
+      method = RequestMethod.DELETE,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE)
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
+  void deleteTaskDefinition(
+      @PathVariable("productidentifier") final String productIdentifier,
+      @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier);
 
   @RequestMapping(
           value = "/products/{productidentifier}/charges/",
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index 708b65a..76eb969 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -15,6 +15,7 @@
  */
 package io.mifos.portfolio.api.v1.domain;
 
+import javax.annotation.Nullable;
 import javax.validation.Valid;
 import java.util.List;
 import java.util.Objects;
@@ -27,6 +28,10 @@
   @Valid
   private List<AccountAssignment> oneTimeAccountAssignments;
 
+  @Valid
+  @Nullable
+  private List<CostComponent> costComponents;
+
   private String note;
   private String createdOn;
   private String createdBy;
@@ -42,6 +47,14 @@
     this.oneTimeAccountAssignments = oneTimeAccountAssignments;
   }
 
+  public List<CostComponent> getCostComponents() {
+    return costComponents;
+  }
+
+  public void setCostComponents(List<CostComponent> costComponents) {
+    this.costComponents = costComponents;
+  }
+
   public String getNote() {
     return note;
   }
@@ -72,23 +85,23 @@
     if (o == null || getClass() != o.getClass()) return false;
     Command command = (Command) o;
     return Objects.equals(oneTimeAccountAssignments, command.oneTimeAccountAssignments) &&
-            Objects.equals(note, command.note) &&
-            Objects.equals(createdOn, command.createdOn) &&
-            Objects.equals(createdBy, command.createdBy);
+        Objects.equals(costComponents, command.costComponents) &&
+        Objects.equals(note, command.note);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(oneTimeAccountAssignments, note, createdOn, createdBy);
+    return Objects.hash(oneTimeAccountAssignments, costComponents, note);
   }
 
   @Override
   public String toString() {
     return "Command{" +
-            "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
-            ", note='" + note + '\'' +
-            ", createdOn='" + createdOn + '\'' +
-            ", createdBy='" + createdBy + '\'' +
-            '}';
+        "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
+        ", costComponents=" + costComponents +
+        ", note='" + note + '\'' +
+        ", createdOn='" + createdOn + '\'' +
+        ", createdBy='" + createdBy + '\'' +
+        '}';
   }
 }
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
index 87ba261..777e819 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
@@ -31,6 +31,7 @@
   String PUT_CASE = "put-case";
   String POST_TASK_DEFINITION = "post-task-definition";
   String PUT_TASK_DEFINITION = "put-task-definition";
+  String DELETE_TASK_DEFINITION = "delete-task-definition";
   String POST_CHARGE_DEFINITION = "post-charge-definition";
   String PUT_CHARGE_DEFINITION = "put-charge-definition";
   String DELETE_PRODUCT_CHARGE_DEFINITION = "delete-product-charge-definition";
@@ -43,6 +44,7 @@
   String SELECTOR_PUT_CASE = SELECTOR_NAME + " = '" + PUT_CASE + "'";
   String SELECTOR_POST_TASK_DEFINITION = SELECTOR_NAME + " = '" + POST_TASK_DEFINITION + "'";
   String SELECTOR_PUT_TASK_DEFINITION = SELECTOR_NAME + " = '" + PUT_TASK_DEFINITION + "'";
+  String SELECTOR_DELETE_TASK_DEFINITION = SELECTOR_NAME + " = '" + DELETE_TASK_DEFINITION + "'";
   String SELECTOR_POST_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + POST_CHARGE_DEFINITION + "'";
   String SELECTOR_PUT_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + PUT_CHARGE_DEFINITION + "'";
   String SELECTOR_DELETE_PRODUCT_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + DELETE_PRODUCT_CHARGE_DEFINITION + "'";
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 4e27123..efca4f2 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -30,6 +30,7 @@
 import io.mifos.portfolio.api.v1.events.CaseEvent;
 import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
 import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
 import io.mifos.portfolio.service.config.PortfolioServiceConfiguration;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 import io.mifos.portfolio.service.internal.util.RhythmAdapter;
@@ -70,7 +71,9 @@
  */
 @RunWith(SpringRunner.class)
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
-        classes = {AbstractPortfolioTest.TestConfiguration.class})
+        classes = {AbstractPortfolioTest.TestConfiguration.class},
+    properties = {"portfolio.bookInterestAsUser=interest_user", "portfolio.bookInterestInTimeSlot=0"}
+)
 public class AbstractPortfolioTest extends SuiteTestEnvironment {
   private static final String LOGGER_NAME = "test-logger";
 
@@ -111,12 +114,15 @@
 
   private AutoUserContext userContext;
 
+  @SuppressWarnings({"SpringAutowiredFieldsWarningInspection", "SpringJavaAutowiringInspection"})
   @Autowired
   protected EventRecorder eventRecorder;
 
+  @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
   @Autowired
   PortfolioManager portfolioManager;
 
+  @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
   @Autowired
   IndividualLending individualLending;
 
@@ -127,6 +133,7 @@
   @MockBean
   LedgerManager ledgerManager;
 
+  @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
   @Autowired
   @Qualifier(LOGGER_NAME)
   Logger logger;
@@ -157,8 +164,7 @@
 
   Product createAndEnableProduct() throws InterruptedException {
     final Product product = createAdjustedProduct(x -> {});
-    portfolioManager.enableProduct(product.getIdentifier(), true);
-    Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+    enableProduct(product);
     return product;
   }
 
@@ -202,7 +208,7 @@
     Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier)));
 
     final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
-    Assert.assertEquals(customerCase.getCurrentState(), nextState.name());
+    Assert.assertEquals(nextState.name(), customerCase.getCurrentState());
   }
 
   boolean individualLoanCommandEventMatches(
@@ -250,4 +256,27 @@
     return entryAccountAssignment;
   }
 
+  TaskDefinition createTaskDefinition(Product product) throws InterruptedException {
+    final TaskDefinition taskDefinition = getTaskDefinition();
+    portfolioManager.createTaskDefinition(product.getIdentifier(), taskDefinition);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+    return taskDefinition;
+  }
+
+  TaskDefinition getTaskDefinition() {
+    final TaskDefinition ret = new TaskDefinition();
+    ret.setIdentifier(Fixture.generateUniqueIdentifer("task"));
+    ret.setDescription("But how do you feel about this?");
+    ret.setName("feep");
+    ret.setMandatory(false);
+    ret.setActions(new HashSet<>());
+    ret.setFourEyes(true);
+    return ret;
+  }
+
+  void enableProduct(final Product product) throws InterruptedException {
+    portfolioManager.enableProduct(product.getIdentifier(), true);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+  }
+
 }
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 d838179..961d20d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -17,6 +17,7 @@
 
 import io.mifos.accounting.api.v1.client.LedgerManager;
 import io.mifos.accounting.api.v1.domain.*;
+import io.mifos.core.lang.DateConverter;
 import org.hamcrest.Description;
 import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentMatcher;
@@ -28,7 +29,10 @@
 import javax.validation.Validation;
 import javax.validation.Validator;
 import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Stream;
 
 import static org.mockito.Matchers.argThat;
 
@@ -37,6 +41,7 @@
  */
 @SuppressWarnings("Duplicates")
 class AccountingFixture {
+  private static final LocalDateTime universalCreationDate = LocalDateTime.of(2017, 7, 18, 15, 16, 43, 10);
   private static final String INCOME_LEDGER_IDENTIFIER = "1000";
   private static final String LOAN_INCOME_LEDGER_IDENTIFIER = "1100";
   private static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
@@ -54,7 +59,37 @@
   static final String LOAN_INTEREST_ACCRUAL_ACCOUNT = "7810";
   static final String CONSUMER_LOAN_INTEREST_ACCOUNT = "1103";
 
-  static final Map<String, Account> accountMap = new HashMap<>();
+  static final Map<String, AccountData> accountMap = new HashMap<>();
+
+  private static class AccountData {
+    final Account account;
+    final List<AccountEntry> accountEntries = new ArrayList<>();
+
+    AccountData(final Account account) {
+      this.account = account;
+    }
+
+    void setBalance(final double balance) {
+      this.account.setBalance(balance);
+    }
+
+    void addAccountEntry(final double amount) {
+      final AccountEntry accountEntry = new AccountEntry();
+      accountEntry.setAmount(amount);
+      accountEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
+      accountEntries.add(accountEntry);
+    }
+  }
+
+  private static void makeAccountResponsive(final Account account, final LocalDateTime creationDate, final LedgerManager ledgerManagerMock) {
+    account.setCreatedOn(DateConverter.toIsoString(creationDate));
+    final AccountData accountData = new AccountData(account);
+    accountMap.put(account.getIdentifier(), accountData);
+    Mockito.doAnswer(new AccountEntriesStreamAnswer(accountData))
+            .when(ledgerManagerMock)
+            .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString());
+
+  }
 
 
   private static Ledger cashLedger() {
@@ -62,6 +97,7 @@
     ret.setIdentifier(CASH_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(ASSET_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
   }
 
@@ -69,6 +105,7 @@
     final Ledger ret = new Ledger();
     ret.setIdentifier(INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
   }
 
@@ -77,6 +114,7 @@
     ret.setIdentifier(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
   }
 
@@ -85,6 +123,7 @@
     ret.setIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
   }
 
@@ -93,6 +132,7 @@
     ret.setIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
   }
 
@@ -101,6 +141,7 @@
     ret.setIdentifier(LOAN_INCOME_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
 
   }
@@ -110,64 +151,65 @@
     ret.setIdentifier(ACCRUED_INCOME_LEDGER_IDENTIFIER);
     ret.setParentLedgerIdentifier(ASSET_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
+    ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
     return ret;
 
   }
 
-  private static void loanFundsSourceAccount() {
+  private static Account loanFundsSourceAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
     ret.setLedger(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    accountMap.put(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, ret);
+    return ret;
   }
 
-  private static void processingFeeIncomeAccount() {
+  private static Account 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());
-    accountMap.put(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
+    return ret;
   }
 
-  private static void loanOriginationFeesIncomeAccount() {
+  private static Account 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());
-    accountMap.put(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, ret);
+    return ret;
   }
 
-  private static void disbursementFeeIncomeAccount() {
+  private static Account 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());
-    accountMap.put(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
+    return ret;
   }
 
-  private static void tellerOneAccount() {
+  private static Account tellerOneAccount() {
     final Account ret = new Account();
     ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
     ret.setLedger(CASH_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    accountMap.put(TELLER_ONE_ACCOUNT_IDENTIFIER, ret);
+    return ret;
   }
 
-  private static void loanInterestAccrualAccount() {
+  private static Account loanInterestAccrualAccount() {
     final Account ret = new Account();
     ret.setIdentifier(LOAN_INTEREST_ACCRUAL_ACCOUNT);
     ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.ASSET.name());
-    accountMap.put(LOAN_INTEREST_ACCRUAL_ACCOUNT, ret);
+    return ret;
   }
 
-  private static void consumerLoanInterestAccount() {
+  private static Account consumerLoanInterestAccount() {
     final Account ret = new Account();
     ret.setIdentifier(CONSUMER_LOAN_INTEREST_ACCOUNT);
     ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
     ret.setType(AccountType.REVENUE.name());
-    accountMap.put(CONSUMER_LOAN_INTEREST_ACCOUNT, ret);
+    return ret;
   }
 
   private static AccountPage customerLoanAccountsPage() {
@@ -188,8 +230,10 @@
   private static Object pendingDisbursalAccountsPage() {
     final Account pendingDisbursalAccount1 = new Account();
     pendingDisbursalAccount1.setIdentifier("pendingDisbursalAccount1");
+
     final Account pendingDisbursalAccount2 = new Account();
     pendingDisbursalAccount2.setIdentifier("pendingDisbursalAccount2");
+
     final Account pendingDisbursalAccount3 = new Account();
     pendingDisbursalAccount3.setIdentifier("pendingDisbursalAccount3");
 
@@ -295,29 +339,42 @@
 
   private static class FindAccountAnswer implements Answer {
     @Override
-    public Account answer(InvocationOnMock invocation) throws Throwable {
+    public Account answer(final InvocationOnMock invocation) throws Throwable {
       final String identifier = invocation.getArgumentAt(0, String.class);
-      return accountMap.get(identifier);
+      return accountMap.get(identifier).account;
     }
   }
 
   private static class CreateAccountAnswer implements Answer {
     @Override
-    public Void answer(InvocationOnMock invocation) throws Throwable {
+    public Void answer(final InvocationOnMock invocation) throws Throwable {
       final Account account = invocation.getArgumentAt(0, Account.class);
-      accountMap.put(account.getIdentifier(), account);
+      makeAccountResponsive(account, LocalDateTime.now(), (LedgerManager) invocation.getMock());
       return null;
     }
   }
 
+  static class AccountEntriesStreamAnswer implements Answer {
+    private final AccountData accountData;
+
+    AccountEntriesStreamAnswer(final AccountData accountData) {
+      this.accountData = accountData;
+    }
+
+    @Override
+    public Stream<AccountEntry> answer(final InvocationOnMock invocation) throws Throwable {
+      return accountData.accountEntries.stream();
+    }
+  }
+
   static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
-    loanFundsSourceAccount();
-    loanOriginationFeesIncomeAccount();
-    processingFeeIncomeAccount();
-    disbursementFeeIncomeAccount();
-    tellerOneAccount();
-    loanInterestAccrualAccount();
-    consumerLoanInterestAccount();
+    makeAccountResponsive(loanFundsSourceAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(loanOriginationFeesIncomeAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(processingFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(disbursementFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(tellerOneAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(loanInterestAccrualAccount(), universalCreationDate, ledgerManagerMock);
+    makeAccountResponsive(consumerLoanInterestAccount(), universalCreationDate, ledgerManagerMock);
 
     Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
     Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
@@ -355,6 +412,8 @@
             Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
             Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())));
     Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
+    accountMap.get(fromAccountIdentifier).addAccountEntry(amount.doubleValue() * -1);
+    accountMap.get(toAccountIdentifier).addAccountEntry(amount.doubleValue());
   }
 
   static void verifyTransfer(final LedgerManager ledgerManager,
@@ -362,5 +421,8 @@
                              final Set<Creditor> creditors) {
     final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors);
     Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
+    debtors.forEach(debtor -> accountMap.get(debtor.getAccountNumber()).addAccountEntry(Double.valueOf(debtor.getAmount())));
+    creditors.forEach(creditor -> accountMap.get(creditor.getAccountNumber()).addAccountEntry(Double.valueOf(creditor.getAmount())));
+
   }
 }
\ No newline at end of file
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 ecf8a64..4d7a7b4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -58,11 +58,13 @@
 
   private BeatListener portfolioBeatListener;
 
-  private static Product product;
-  private static Case customerCase;
-  private static CaseParameters caseParameters;
-  private static String pendingDisbursalAccountIdentifier;
-  private static String customerLoanAccountIdentifier;
+  private Product product = null;
+  private Case customerCase = null;
+  private CaseParameters caseParameters = null;
+  private String pendingDisbursalAccountIdentifier = null;
+  private String customerLoanAccountIdentifier = null;
+
+  private BigDecimal expectedCurrentBalance = null;
 
 
   @Before
@@ -86,7 +88,7 @@
     step4ApproveCase();
     step5DisburseFullAmount();
     step6CalculateInterestAccrual();
-    //step7PaybackFullAmount();
+    step7PaybackFullAmount();
   }
 
   //Create product and set charges to fixed fees.
@@ -182,6 +184,8 @@
     creditors.add(new Creditor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
     creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
     AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+    expectedCurrentBalance = BigDecimal.ZERO;
   }
 
   //Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
@@ -207,6 +211,7 @@
     creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
     AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
 
+    expectedCurrentBalance = expectedCurrentBalance.add(caseParameters.getMaximumBalance());
   }
 
   //Perform daily interest calculation.
@@ -215,7 +220,7 @@
     final String beatIdentifier = "alignment0";
     final String midnightTimeStamp = DateConverter.toIsoString(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS));
 
-    AccountingFixture.mockBalance(customerLoanAccountIdentifier, caseParameters.getMaximumBalance());
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
 
     final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
     portfolioBeatListener.publishBeat(interestBeat);
@@ -228,19 +233,44 @@
     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 BigDecimal 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);
 
     final Set<Debtor> debtors = new HashSet<>();
     debtors.add(new Debtor(
         AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT,
-        calculatedInterest));
+        calculatedInterest.toPlainString()));
 
     final Set<Creditor> creditors = new HashSet<>();
     creditors.add(new Creditor(
         customerLoanAccountIdentifier,
-        calculatedInterest));
+        calculatedInterest.toPlainString()));
     AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+    expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
+  }
+
+  private void step7PaybackFullAmount() throws InterruptedException {
+    logger.info("step7PaybackFullAmount");
+
+    AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+
+    checkStateTransfer(
+        product.getIdentifier(),
+        customerCase.getIdentifier(),
+        Action.ACCEPT_PAYMENT,
+        Collections.singletonList(assignEntryToTeller()),
+        IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
+        Case.State.CLOSED);
+    checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
+
+    final Set<Debtor> debtors = new HashSet<>();
+    debtors.add(new Debtor(customerLoanAccountIdentifier, expectedCurrentBalance.toPlainString()));
+
+    final Set<Creditor> creditors = new HashSet<>();
+    creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.toPlainString()));
+    AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+    expectedCurrentBalance = expectedCurrentBalance.subtract(expectedCurrentBalance);
   }
 }
\ No newline at end of file
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 96944a7..5e47c01 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -15,6 +15,8 @@
  */
 package io.mifos.portfolio;
 
+import io.mifos.accounting.api.v1.domain.AccountEntry;
+import io.mifos.core.lang.DateConverter;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
@@ -23,9 +25,13 @@
 import io.mifos.portfolio.api.v1.domain.Product;
 import org.junit.Assert;
 import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
 
+import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Stream;
 
 import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
 
@@ -70,6 +76,13 @@
     checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
             Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
 
+    final AccountEntry firstEntry = new AccountEntry();
+    firstEntry.setAmount(2000.0);
+    firstEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
+    Mockito.doAnswer((x) -> Stream.of(firstEntry))
+        .when(ledgerManager)
+        .fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString());
+
 
     checkStateTransfer(
         product.getIdentifier(),
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
index 0472a75..25438b6 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
@@ -15,6 +15,8 @@
  */
 package io.mifos.portfolio;
 
+import io.mifos.core.api.util.NotFoundException;
+import io.mifos.portfolio.api.v1.client.ProductInUseException;
 import io.mifos.portfolio.api.v1.domain.Product;
 import io.mifos.portfolio.api.v1.domain.TaskDefinition;
 import io.mifos.portfolio.api.v1.events.EventConstants;
@@ -22,7 +24,6 @@
 import org.junit.Assert;
 import org.junit.Test;
 
-import java.util.HashSet;
 import java.util.List;
 
 /**
@@ -52,12 +53,29 @@
 
     portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION,
-            new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+        new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
 
     final TaskDefinition taskDefinitionRead = portfolioManager.getTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
     Assert.assertEquals(taskDefinition,taskDefinitionRead);
   }
 
+  @Test(expected = ProductInUseException.class)
+  public void shouldNotChangeTaskDefinitionForProductWithCases() throws InterruptedException {
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+    enableProduct(product);
+
+    createCase(product.getIdentifier());
+
+    taskDefinition.setDescription("bleblablub");
+    taskDefinition.setFourEyes(false);
+
+    portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
+    Assert.assertFalse(this.eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION,
+        new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+  }
+
   @Test
   public void shouldAddTaskDefinition() throws InterruptedException {
     final Product product = createProduct();
@@ -71,21 +89,41 @@
     Assert.assertTrue(tasks.size() == initialTaskCount + 1);
   }
 
-  private TaskDefinition createTaskDefinition(Product product) throws InterruptedException {
+  @Test(expected = ProductInUseException.class)
+  public void shouldNotCreateTaskDefinitionForProductWithCases() throws InterruptedException {
+    final Product product = createAndEnableProduct();
+    createCase(product.getIdentifier());
+
     final TaskDefinition taskDefinition = getTaskDefinition();
     portfolioManager.createTaskDefinition(product.getIdentifier(), taskDefinition);
-    Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
-    return taskDefinition;
+    Assert.assertFalse(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
   }
 
-  private TaskDefinition getTaskDefinition() {
-    final TaskDefinition ret = new TaskDefinition();
-    ret.setIdentifier(Fixture.generateUniqueIdentifer("task"));
-    ret.setDescription("But how do you feel about this?");
-    ret.setName("feep");
-    ret.setMandatory(false);
-    ret.setActions(new HashSet<>());
-    ret.setFourEyes(true);
-    return ret;
+  @Test
+  public void shouldDeleteTaskDefinition() throws InterruptedException {
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+    portfolioManager.deleteTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.DELETE_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+
+    try {
+      portfolioManager.getTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+      Assert.fail();
+    }
+    catch (final NotFoundException ignored) {
+    }
+  }
+
+  @Test(expected = ProductInUseException.class)
+  public void shouldNotDeleteTaskDefinitionForProductWithCases() throws InterruptedException {
+    final Product product = createProduct();
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+    enableProduct(product);
+    createCase(product.getIdentifier());
+
+    portfolioManager.deleteTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+    Assert.assertFalse(this.eventRecorder.wait(EventConstants.DELETE_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
   }
 }
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
index 6865b99..adaf189 100644
--- a/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
@@ -50,12 +50,22 @@
   }
 
   @JmsListener(
-          subscription = EventConstants.DESTINATION,
-          destination = EventConstants.DESTINATION,
-          selector = EventConstants.SELECTOR_PUT_TASK_DEFINITION
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_PUT_TASK_DEFINITION
   )
   public void onChangeTaskDefinition(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
-                              final String payload) {
+                                     final String payload) {
     this.eventRecorder.event(tenant, EventConstants.PUT_TASK_DEFINITION, payload, TaskDefinitionEvent.class);
   }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_DELETE_TASK_DEFINITION
+  )
+  public void onDeleteTaskDefinition(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                     final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.DELETE_TASK_DEFINITION, payload, TaskDefinitionEvent.class);
+  }
 }
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 f72ca48..549dc9b 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
@@ -23,6 +23,7 @@
 import io.mifos.core.lang.ServiceException;
 import io.mifos.individuallending.IndividualLendingPatternFactory;
 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.api.v1.events.IndividualLoanCommandEvent;
 import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
@@ -41,11 +42,13 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.annotation.Nullable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -90,13 +93,17 @@
             = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
 
     final List<ChargeInstance> charges = costComponents.stream()
-        .map(x -> mapCostComponentEntryToChargeInstance(Action.OPEN, x, designatorToAccountIdentifierMapper))
+        .map(entry -> mapCostComponentEntryToChargeInstance(
+            Action.OPEN,
+            entry,
+            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+            designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
-            command.getCommand().getNote(),
-            productIdentifier + "." + caseIdentifier + "." + Action.OPEN.name(),
-            Action.OPEN.getTransactionType());
+        command.getCommand().getNote(),
+        dataContextOfAction.getMessageForCharge(Action.OPEN),
+        Action.OPEN.getTransactionType());
     //Only move to new state if book charges command was accepted.
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
     customerCase.setCurrentState(Case.State.PENDING.name());
@@ -124,7 +131,11 @@
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
 
     final List<ChargeInstance> charges = costComponents.stream()
-        .map(x -> mapCostComponentEntryToChargeInstance(Action.DENY, x, designatorToAccountIdentifierMapper))
+        .map(entry -> mapCostComponentEntryToChargeInstance(
+            Action.DENY,
+            entry,
+            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+            designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
@@ -165,13 +176,17 @@
         costComponentService.getCostComponentsForApprove(dataContextOfAction);
 
     final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
-        .map(x -> mapCostComponentEntryToChargeInstance(Action.APPROVE, x, designatorToAccountIdentifierMapper))
+        .map(entry -> mapCostComponentEntryToChargeInstance(
+            Action.APPROVE,
+            entry,
+            getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+            designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
-            command.getCommand().getNote(),
-            productIdentifier + "." + caseIdentifier + "." + Action.APPROVE.name(),
-            Action.APPROVE.getTransactionType());
+        command.getCommand().getNote(),
+        dataContextOfAction.getMessageForCharge(Action.APPROVE),
+        Action.APPROVE.getTransactionType());
 
     //Only move to new state if book charges command was accepted.
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
@@ -200,13 +215,18 @@
 
     final BigDecimal disbursalAmount = dataContextOfAction.getCaseParameters().getMaximumBalance();
     final List<ChargeInstance> charges = Stream.concat(
-          costComponentsForRepaymentPeriod.stream().map(x -> mapCostComponentEntryToChargeInstance(Action.DISBURSE, x, designatorToAccountIdentifierMapper)),
+          costComponentsForRepaymentPeriod.stream()
+              .map(entry -> mapCostComponentEntryToChargeInstance(
+                  Action.DISBURSE,
+                  entry,
+                  getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+                  designatorToAccountIdentifierMapper)),
           Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
         .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
         command.getCommand().getNote(),
-        productIdentifier + "." + caseIdentifier + "." + Action.DISBURSE.name(),
+        dataContextOfAction.getMessageForCharge(Action.DISBURSE),
         Action.DISBURSE.getTransactionType());
     //Only move to new state if book charges command was accepted.
     if (Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()) != Case.State.ACTIVE) {
@@ -244,12 +264,16 @@
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
 
     final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
-        .map(x -> mapCostComponentEntryToChargeInstance(Action.APPLY_INTEREST, x, designatorToAccountIdentifierMapper))
+        .map(entry -> mapCostComponentEntryToChargeInstance(
+            Action.APPLY_INTEREST,
+            entry,
+            Collections.emptyMap(),
+            designatorToAccountIdentifierMapper))
         .collect(Collectors.toList());
 
     accountingAdapter.bookCharges(charges,
         "",
-        productIdentifier + "." + caseIdentifier + "." + Action.APPLY_INTEREST.name(),
+        dataContextOfAction.getMessageForCharge(Action.APPLY_INTEREST),
         Action.APPLY_INTEREST.getTransactionType());
 
     return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
@@ -257,15 +281,57 @@
 
   @Transactional
   @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
-  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
+  @EventEmitter(
+      selectorName = EventConstants.SELECTOR_NAME,
+      selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
   public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
     final String productIdentifier = command.getProductIdentifier();
     final String caseIdentifier = command.getCaseIdentifier();
-    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+    final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+        productIdentifier, caseIdentifier, null);
     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);
+    if (dataContextOfAction.getCustomerCase().getEndOfTerm() == null)
+      throw ServiceException.internalError(
+          "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
+
+    final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+        costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction);
+
+    final Map<String, BigDecimal> requestedChargeAmounts
+        = getRequestedChargeAmounts(command.getCommand().getCostComponents());
+
+    final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
+        .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
+        .map(entry -> getChargeAmount(
+            requestedChargeAmounts.get(entry.getKey().getIdentifier()),
+            entry.getValue().getAmount()))
+        .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+        = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+    final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
+        .map(entry -> mapCostComponentEntryToChargeInstance(
+            Action.ACCEPT_PAYMENT,
+            entry,
+            requestedChargeAmounts,
+            designatorToAccountIdentifierMapper))
+        .collect(Collectors.toList());
+
+
+    accountingAdapter.bookCharges(charges,
+        command.getCommand().getNote(),
+        dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT),
+        Action.ACCEPT_PAYMENT.getTransactionType());
+
+    final BigDecimal newBalance = costComponentsForRepaymentPeriod.getRunningBalance()
+        .add(sumOfAdjustments);
+    if (newBalance.compareTo(BigDecimal.ZERO) <= 0) {
+      final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
+      //TODO: customerCase.setCurrentState(Case.State.CLOSED.name());
+      caseRepository.save(customerCase);
+    }
+
     return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
 
@@ -311,29 +377,41 @@
     return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
   }
 
-
   private static ChargeInstance mapCostComponentEntryToChargeInstance(
       final Action action,
       final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
+      final Map<String, BigDecimal> requestedChargeAmounts,
       final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
     final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
+
+    final BigDecimal requestedChargeAmount = requestedChargeAmounts.get(chargeDefinition.getIdentifier());
+    final BigDecimal configuredChargeAmount = costComponentEntry.getValue().getAmount();
+
+    final BigDecimal finalChargeAmount = getChargeAmount(requestedChargeAmount, configuredChargeAmount);
+
     if (chargeDefinition.getAccrualAccountDesignator() != null) {
       if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
         return new ChargeInstance(
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
-            costComponentEntry.getValue().getAmount());
+            finalChargeAmount);
       else
         return new ChargeInstance(
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
             designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
-            costComponentEntry.getValue().getAmount());
+            finalChargeAmount);
     }
     else
       return new ChargeInstance(
           designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
           designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
-          costComponentEntry.getValue().getAmount());
+          finalChargeAmount);
+  }
+
+  private static BigDecimal getChargeAmount(
+      final BigDecimal requestedChargeAmount,
+      final BigDecimal configuredChargeAmount) {
+    return requestedChargeAmount != null ? requestedChargeAmount : configuredChargeAmount;
   }
 
   private static ChargeInstance getDisbursalChargeInstance(
@@ -344,4 +422,16 @@
         designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
         amount);
   }
+
+  private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
+    if (costComponents == null)
+      return Collections.emptyMap();
+    else
+      return costComponents.stream()
+          .collect(Collectors.groupingBy(
+              CostComponent::getChargeIdentifier,
+              Collectors.reducing(BigDecimal.ZERO,
+                  CostComponent::getAmount,
+                  BigDecimal::add)));
+  }
 }
\ 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 bccade4..573a405 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
@@ -36,6 +36,7 @@
 import javax.annotation.Nullable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
 import java.util.function.BiFunction;
@@ -179,8 +180,9 @@
             minorCurrencyUnitDigits);
   }
 
-  public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(final DataContextOfAction dataContextOfAction) {
-
+  public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(
+      final DataContextOfAction dataContextOfAction)
+  {
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
         = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
     final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
@@ -189,19 +191,80 @@
     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);
+    final LocalDate today = today();
+    final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, today, new Period(1, today));
+
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier,
+        minorCurrencyUnitDigits,
+        currentBalance,
+        Collections.singletonList(interestAction));
 
     return getCostComponentsForScheduledCharges(
-            scheduledCharges,
-            caseParameters.getMaximumBalance(),
-            currentBalance,
-            minorCurrencyUnitDigits);
+        scheduledCharges,
+        caseParameters.getMaximumBalance(),
+        currentBalance,
+        minorCurrencyUnitDigits);
   }
 
-  private CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(final DataContextOfAction dataContextOfAction) {
-    return null;
+  public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
+      final DataContextOfAction dataContextOfAction)
+  {
+    final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+        = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+    final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+    final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+
+    final String interestAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.INTEREST_ACCRUAL);
+    final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+    final BigDecimal interestAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
+        interestAccrualAccountIdentifier,
+        startOfTerm,
+        dataContextOfAction.getMessageForCharge(Action.APPLY_INTEREST));
+    final BigDecimal interestApplied = accountingAdapter.sumMatchingEntriesSinceDate(
+        interestAccrualAccountIdentifier,
+        startOfTerm,
+        dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
+    final BigDecimal interestOutstanding = interestAccrued.subtract(interestApplied);
+
+    final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
+    final List<ScheduledAction> scheduledActions
+        = ScheduledActionHelpers.getNextScheduledActionForDisbursedLoan(
+            startOfTerm,
+            dataContextOfAction.getCustomerCase().getEndOfTerm().toLocalDate(),
+            caseParameters,
+            Action.ACCEPT_PAYMENT
+        )
+        .map(Collections::singletonList)
+        .orElse(Collections.emptyList());
+
+    final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+        productIdentifier,
+        minorCurrencyUnitDigits,
+        currentBalance,
+        scheduledActions);
+
+    return getCostComponentsForScheduledCharges(
+        scheduledCharges,
+        caseParameters.getMaximumBalance(),
+        currentBalance,
+        minorCurrencyUnitDigits);
   }
+
+  private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
+                                          final String customerLoanAccountIdentifier) {
+    final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
+        customerLoanAccountIdentifier,
+        dataContextOfAction.getMessageForCharge(Action.DISBURSE));
+
+    return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
+        .orElseThrow(() -> ServiceException.internalError(
+            "Start of term for loan ''{0}'' could not be acquired from accounting.",
+            dataContextOfAction.getCompoundIdentifer()));
+  }
+
   private CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction) {
     return null;
   }
@@ -243,6 +306,7 @@
     }
 
     return new CostComponentsForRepaymentPeriod(
+        runningBalance,
         costComponentMap,
         balanceAdjustment);
   }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
index a51e975..defa570 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
@@ -26,17 +26,32 @@
  * @author Myrle Krantz
  */
 public class CostComponentsForRepaymentPeriod {
-  final Map<ChargeDefinition, CostComponent> costComponents;
-  final BigDecimal balanceAdjustment;
+  final private BigDecimal runningBalance;
+  final private Map<ChargeDefinition, CostComponent> costComponents;
+  final private BigDecimal balanceAdjustment;
 
   CostComponentsForRepaymentPeriod(
-          final Map<ChargeDefinition, CostComponent> costComponents,
-          final BigDecimal balanceAdjustment) {
+      final BigDecimal runningBalance,
+      final Map<ChargeDefinition, CostComponent> costComponents,
+      final BigDecimal balanceAdjustment) {
+    this.runningBalance = runningBalance;
     this.costComponents = costComponents;
     this.balanceAdjustment = balanceAdjustment;
   }
 
+  public BigDecimal getRunningBalance() {
+    return runningBalance;
+  }
+
+  Map<ChargeDefinition, CostComponent> getCostComponents() {
+    return costComponents;
+  }
+
   public Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
     return costComponents.entrySet().stream();
   }
+
+  BigDecimal getBalanceAdjustment() {
+    return balanceAdjustment;
+  }
 }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
index 962efb2..f7c2485 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
@@ -16,6 +16,7 @@
 package io.mifos.individuallending.internal.service;
 
 import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
 import io.mifos.portfolio.service.internal.repository.CaseEntity;
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
@@ -56,7 +57,15 @@
     return caseParameters;
   }
 
-  public @Nonnull List<AccountAssignment> getOneTimeAccountAssignments() {
+  @Nonnull List<AccountAssignment> getOneTimeAccountAssignments() {
     return oneTimeAccountAssignments;
   }
+
+  String getCompoundIdentifer() {
+    return product.getIdentifier() + "." + customerCase.getIdentifier();
+  }
+
+  public String getMessageForCharge(final Action action) {
+    return getCompoundIdentifer() + "." + action.name();
+  }
 }
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 e7288a2..5dcf51a 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
@@ -199,9 +199,9 @@
               CostComponentService.getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
 
       final PlannedPayment plannedPayment = new PlannedPayment();
-      plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.costComponents.values()));
+      plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
       plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
-      balance = balance.add(costComponentsForRepaymentPeriod.balanceAdjustment);
+      balance = balance.add(costComponentsForRepaymentPeriod.getBalanceAdjustment());
       plannedPayment.setRemainingPrincipal(balance);
       plannedPayments.add(plannedPayment);
     }
@@ -234,9 +234,14 @@
                                                     final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
                                                     final ChargeDefinition acceptPaymentDefinition) {
     return scheduledActions.stream()
-            .flatMap(scheduledAction -> getChargeDefinitionStream(chargeDefinitionsMappedByChargeAction, chargeDefinitionsMappedByAccrueAction, acceptPaymentDefinition, scheduledAction)
-                    .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
-            .collect(Collectors.toList());
+        .flatMap(scheduledAction ->
+            getChargeDefinitionStream(
+                chargeDefinitionsMappedByChargeAction,
+                chargeDefinitionsMappedByAccrueAction,
+                acceptPaymentDefinition,
+                scheduledAction)
+                .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
+        .collect(Collectors.toList());
   }
 
   private Stream<ChargeDefinition> getChargeDefinitionStream(
@@ -244,20 +249,25 @@
           final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
           final ChargeDefinition acceptPaymentDefinition,
           final ScheduledAction scheduledAction) {
-    List<ChargeDefinition> chargeMapping = chargeDefinitionsMappedByChargeAction.get(scheduledAction.action.name());
-    if ((chargeMapping == null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction())))
-      chargeMapping = Collections.singletonList(acceptPaymentDefinition);
-
+    final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
+        .get(scheduledAction.action.name());
+    Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
     if (chargeMapping == null)
-      chargeMapping = Collections.emptyList();
+      chargeMapping = Stream.empty();
 
-    List<ChargeDefinition> accrueMapping = chargeDefinitionsMappedByAccrueAction.get(scheduledAction.action.name());
-    if ((accrueMapping == null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction())))
-      accrueMapping = Collections.singletonList(acceptPaymentDefinition);
+    if (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction()))
+      chargeMapping = Stream.concat(chargeMapping, Stream.of(acceptPaymentDefinition));
 
+    final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
+        .get(scheduledAction.action.name());
+    Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
     if (accrueMapping == null)
-      accrueMapping = Collections.emptyList();
+      accrueMapping = Stream.empty();
 
-    return Stream.concat(accrueMapping.stream(), chargeMapping.stream());
+    if ((acceptPaymentDefinition.getAccrueAction() != null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getAccrueAction())))
+      accrueMapping = Stream.concat(chargeMapping, Stream.of(acceptPaymentDefinition));
+
+
+    return Stream.concat(accrueMapping, chargeMapping);
   }
 }
\ No newline at end of file
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 ecd04f2..7718553 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
@@ -40,6 +40,11 @@
     this.endDate = beginDate.plusDays(periodLength);
   }
 
+  Period(final int periodLength, final LocalDate endDate) {
+    this.beginDate = endDate.minusDays(periodLength);
+    this.endDate = endDate;
+  }
+
   LocalDate getBeginDate() {
     return beginDate;
   }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index 3466c21..d6186a7 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -44,7 +44,8 @@
   private static boolean accruedCharge(final ScheduledCharge scheduledCharge)
   {
     return scheduledCharge.getChargeDefinition().getAccrualAccountDesignator() != null &&
-            scheduledCharge.getChargeDefinition().getAccrueAction() != null;
+        scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
+        scheduledCharge.getScheduledAction().repaymentPeriod != null;
   }
 
   static BigDecimal chargeAmountPerPeriod(final ScheduledCharge scheduledCharge, final int precision)
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 30c77fe..1d85ecb 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
@@ -42,6 +42,15 @@
   }
 
   ScheduledAction(@Nonnull final Action action,
+                  @Nonnull final LocalDate when,
+                  @Nonnull final Period actionPeriod) {
+    this.action = action;
+    this.when = when;
+    this.actionPeriod = actionPeriod;
+    this.repaymentPeriod = null;
+  }
+
+  ScheduledAction(@Nonnull final Action action,
                   @Nonnull final LocalDate when) {
     this.action = action;
     this.when = when;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
index 80dcf48..e310af1 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
@@ -15,9 +15,9 @@
  */
 package io.mifos.individuallending.internal.service;
 
-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 io.mifos.portfolio.api.v1.domain.PaymentCycle;
 
 import javax.annotation.Nonnull;
 import java.time.DayOfWeek;
@@ -25,9 +25,7 @@
 import java.time.YearMonth;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -37,57 +35,61 @@
 @SuppressWarnings("WeakerAccess")
 public class ScheduledActionHelpers {
   public static boolean actionHasNoActionPeriod(final Action action) {
-    return preDisbursalActions().anyMatch(x -> action == x);
+    return preTermActions().anyMatch(x -> action == x) || postTermActions().anyMatch(x -> action == x);
   }
 
-  private static Stream<Action> preDisbursalActions() {
+  private static Stream<Action> preTermActions() {
     return Stream.of(Action.OPEN, Action.APPROVE, Action.DISBURSE);
   }
 
-  public static List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
+  private static Stream<Action> postTermActions() {
+    return Stream.of(Action.CLOSE);
+  }
+
+  public static List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate startOfTerm,
                                                         final @Nonnull CaseParameters caseParameters)
   {
-    final LocalDate endOfTerm = getRoughEndDate(initialDisbursalDate, caseParameters);
-    return Stream.concat(preDisbursalActions().map(action -> new ScheduledAction(action, initialDisbursalDate)),
-        getHypotheticalScheduledActionsForDisbursedLoan(initialDisbursalDate, endOfTerm, caseParameters))
+    final LocalDate endOfTerm = getRoughEndDate(startOfTerm, caseParameters);
+    return Stream.concat( Stream.concat(
+          preTermActions().map(action -> new ScheduledAction(action, startOfTerm)),
+          getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, endOfTerm, caseParameters)),
+          postTermActions().map(action -> new ScheduledAction(action, endOfTerm)))
         .collect(Collectors.toList());
   }
 
-  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.");
+  public static Optional<ScheduledAction> getNextScheduledActionForDisbursedLoan(final @Nonnull LocalDate startOfTerm,
+                                                                                 final @Nonnull LocalDate endOfTerm,
+                                                                                 final @Nonnull CaseParameters caseParameters,
+                                                                                 final @Nonnull Action action) {
+    if (preTermActions().anyMatch(x -> action == x))
+      throw new IllegalStateException("Should not be calling getNextScheduledActionsForDisbursedLoan with an action which occurs before disbursement.");
 
-    final LocalDate today = LocalDate.now(ZoneId.of("UTC"));
-    return getHypotheticalScheduledActionsForDisbursedLoan(today, endOfTerm, caseParameters)
+    final LocalDate now = LocalDate.now(ZoneId.of("UTC"));
+    return getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, endOfTerm, caseParameters)
         .filter(x -> x.action.equals(action))
-        .filter(x -> x.actionPeriod != null && x.actionPeriod.containsDate(forDate))
-        .collect(Collectors.toList());
+        .filter(x -> x.actionPeriod != null && x.actionPeriod.containsDate(now))
+        .sorted(Comparator.comparing(x -> x.actionPeriod))
+        .findFirst();
   }
 
   private static Stream<ScheduledAction> getHypotheticalScheduledActionsForDisbursedLoan(
-      final @Nonnull LocalDate initialDisbursalDate,
+      final @Nonnull LocalDate startOfTerm,
       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)));
+    return generateRepaymentPeriods(startOfTerm, endOfTerm, caseParameters)
+        .flatMap(ScheduledActionHelpers::generateScheduledActionsForRepaymentPeriod);
   }
 
   /** '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,
+  public static LocalDate getRoughEndDate(final @Nonnull LocalDate startOfTerm,
                                           final @Nonnull CaseParameters caseParameters) {
     final Integer maximumTermSize = caseParameters.getTermRange().getMaximum();
     final ChronoUnit termUnit = caseParameters.getTermRange().getTemporalUnit();
 
-    return initialDisbursalDate.plus(
+    return startOfTerm.plus(
             maximumTermSize,
             termUnit);
   }
@@ -107,24 +109,24 @@
             .limit(ChronoUnit.DAYS.between(repaymentPeriod.getBeginDate(), repaymentPeriod.getEndDate()));
   }
 
-  private static SortedSet<Period> generateRepaymentPeriods(
-          final LocalDate initialDisbursalDate,
-          final LocalDate endDate,
+  private static Stream<Period> generateRepaymentPeriods(
+          final LocalDate startOfTerm,
+          final LocalDate endOfTerm,
           final CaseParameters caseParameters) {
 
-    final SortedSet<Period> ret = new TreeSet<>();
-    LocalDate lastPaymentDate = initialDisbursalDate;
-    LocalDate nextPaymentDate = generateNextPaymentDate(caseParameters, initialDisbursalDate);
-    while (nextPaymentDate.isBefore(endDate))
+    final List<Period> ret = new ArrayList<>();
+    LocalDate lastPaymentDate = startOfTerm;
+    LocalDate nextPaymentDate = generateNextPaymentDate(caseParameters, lastPaymentDate);
+    while (nextPaymentDate.isBefore(endOfTerm))
     {
       final Period period = new Period(lastPaymentDate, nextPaymentDate);
       ret.add(period);
       lastPaymentDate = nextPaymentDate;
-      nextPaymentDate = generateNextPaymentDate(caseParameters, nextPaymentDate);
+      nextPaymentDate = generateNextPaymentDate(caseParameters, lastPaymentDate);
     }
     ret.add(new Period(lastPaymentDate, nextPaymentDate));
 
-    return ret;
+    return ret.stream();
   }
 
   private static LocalDate generateNextPaymentDate(final CaseParameters caseParameters, final LocalDate lastPaymentDate) {
@@ -150,10 +152,10 @@
     return alignPaymentDate(orientedPaymentDate, maximumAlignmentChronoUnit, paymentCycle);
   }
 
-  private static LocalDate incrementPaymentDate(LocalDate paymentDate, PaymentCycle paymentCycle) {
+  private static LocalDate incrementPaymentDate(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
     return paymentDate.plus(
-            paymentCycle.getPeriod(),
-            paymentCycle.getTemporalUnit());
+        paymentCycle.getPeriod(),
+        paymentCycle.getTemporalUnit());
   }
 
   private static LocalDate orientPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumSpecifiedAlignmentChronoUnit, PaymentCycle paymentCycle) {
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.java
new file mode 100644
index 0000000..8fdba76
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.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.portfolio.service.internal.command;
+
+/**
+ * @author Myrle Krantz
+ */
+public class DeleteTaskDefinitionCommand {
+  private final String productIdentifier;
+  private final String taskIdentifier;
+
+  public DeleteTaskDefinitionCommand(final String productIdentifier, final String taskIdentifier) {
+    this.productIdentifier = productIdentifier;
+    this.taskIdentifier = taskIdentifier;
+  }
+
+  public String getProductIdentifier() {
+    return productIdentifier;
+  }
+
+  public String getTaskIdentifier() {
+    return taskIdentifier;
+  }
+
+  @Override
+  public String toString() {
+    return "DeleteTaskDefinitionCommand{" +
+        "productIdentifier='" + productIdentifier + '\'' +
+        ", taskIdentifier='" + taskIdentifier + '\'' +
+        '}';
+  }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
index 039ba68..6f672d2 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
@@ -21,6 +21,7 @@
 import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
 import io.mifos.portfolio.service.internal.command.ChangeTaskDefinitionCommand;
 import io.mifos.portfolio.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.command.DeleteTaskDefinitionCommand;
 import io.mifos.portfolio.service.internal.mapper.TaskDefinitionMapper;
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
 import io.mifos.portfolio.service.internal.repository.ProductRepository;
@@ -75,18 +76,33 @@
     final String productIdentifier = changeTaskDefinitionCommand.getProductIdentifier();
 
     final TaskDefinitionEntity existingTaskDefinition
-            = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskDefinition.getIdentifier())
-            .orElseThrow(() -> ServiceException.internalError("task definition not found."));
+        = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskDefinition.getIdentifier())
+        .orElseThrow(() -> ServiceException.internalError("task definition not found."));
 
     final TaskDefinitionEntity taskDefinitionEntity =
-            TaskDefinitionMapper.map(existingTaskDefinition.getProduct(), taskDefinition);
+        TaskDefinitionMapper.map(existingTaskDefinition.getProduct(), taskDefinition);
     taskDefinitionEntity.setId(existingTaskDefinition.getId());
     taskDefinitionEntity.setId(existingTaskDefinition.getId());
     taskDefinitionRepository.save(taskDefinitionEntity);
 
     return new TaskDefinitionEvent(
-            changeTaskDefinitionCommand.getProductIdentifier(),
-            changeTaskDefinitionCommand.getInstance().getIdentifier());
+        changeTaskDefinitionCommand.getProductIdentifier(),
+        changeTaskDefinitionCommand.getInstance().getIdentifier());
+  }
+
+  @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.DELETE_TASK_DEFINITION)
+  public TaskDefinitionEvent process(final DeleteTaskDefinitionCommand deleteTaskDefinitionCommand) {
+    final String productIdentifier = deleteTaskDefinitionCommand.getProductIdentifier();
+    final String taskIdentifier = deleteTaskDefinitionCommand.getTaskIdentifier();
+
+    final TaskDefinitionEntity existingTaskDefinition
+        = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskIdentifier)
+        .orElseThrow(() -> ServiceException.notFound("Task definition ''{0}.{1}'' not found.", productIdentifier, taskIdentifier));
+
+    taskDefinitionRepository.delete(existingTaskDefinition);
+
+    return new TaskDefinitionEvent(productIdentifier, taskIdentifier);
   }
 
 }
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 755640f..d504b23 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.DateRange;
 import io.mifos.core.lang.ServiceException;
 import io.mifos.portfolio.api.v1.domain.AccountAssignment;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
@@ -29,7 +30,9 @@
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -41,7 +44,6 @@
  */
 @Component
 public class AccountingAdapter {
-
   public enum IdentifierType {LEDGER, ACCOUNT}
 
   private final LedgerManager ledgerManager;
@@ -79,6 +81,26 @@
     ledgerManager.createJournalEntry(journalEntry);
   }
 
+  public Optional<LocalDateTime> getDateOfOldestEntryContainingMessage(final String accountIdentifier,
+                                                                       final String message) {
+    final Account account = ledgerManager.findAccount(accountIdentifier);
+    final LocalDateTime accountCreatedOn = DateConverter.fromIsoString(account.getCreatedOn());
+    final DateRange fromAccountCreationUntilNow = oneSidedDateRange(accountCreatedOn.toLocalDate());
+
+    return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message)
+        .findFirst()
+        .map(AccountEntry::getTransactionDate)
+        .map(DateConverter::fromIsoString);
+  }
+
+  public BigDecimal sumMatchingEntriesSinceDate(final String accountIdentifier, final LocalDate startDate, final String message)
+  {
+    final DateRange fromLastPaymentUntilNow = oneSidedDateRange(startDate);
+    return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromLastPaymentUntilNow.toString(), message)
+        .map(AccountEntry::getAmount)
+        .map(BigDecimal::valueOf).reduce(BigDecimal.ZERO, BigDecimal::add);
+  }
+
   private static Optional<Debtor> mapToDebtor(final ChargeInstance chargeInstance) {
     if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
       return Optional.empty();
@@ -187,4 +209,8 @@
     else
       return false;
   }
+
+  private static DateRange oneSidedDateRange(final LocalDate start) {
+    return new DateRange(start, LocalDate.now(ZoneId.of("UTC")));
+  }
 }
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
index c524cbc..18c5439 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
@@ -17,14 +17,16 @@
 
 import io.mifos.anubis.annotation.AcceptedTokenType;
 import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.ServiceException;
 import io.mifos.portfolio.api.v1.PermittableGroupIds;
 import io.mifos.portfolio.api.v1.domain.TaskDefinition;
 import io.mifos.portfolio.service.internal.command.ChangeTaskDefinitionCommand;
 import io.mifos.portfolio.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.command.DeleteTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.service.CaseService;
 import io.mifos.portfolio.service.internal.service.ProductService;
 import io.mifos.portfolio.service.internal.service.TaskDefinitionService;
-import io.mifos.core.command.gateway.CommandGateway;
-import io.mifos.core.lang.ServiceException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -44,17 +46,20 @@
   private final CommandGateway commandGateway;
   private final TaskDefinitionService taskDefinitionService;
   private final ProductService productService;
+  private final CaseService caseService;
 
   @Autowired
   public TaskDefinitionRestController(
-          final CommandGateway commandGateway,
-          final TaskDefinitionService taskDefinitionService,
-          final ProductService productService)
+      final CommandGateway commandGateway,
+      final TaskDefinitionService taskDefinitionService,
+      final ProductService productService,
+      final CaseService caseService)
   {
     super();
     this.commandGateway = commandGateway;
     this.taskDefinitionService = taskDefinitionService;
     this.productService = productService;
+    this.caseService = caseService;
   }
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@@ -73,9 +78,9 @@
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
   @RequestMapping(
-          method = RequestMethod.POST,
-          consumes = MediaType.APPLICATION_JSON_VALUE,
-          produces = MediaType.APPLICATION_JSON_VALUE
+      method = RequestMethod.POST,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
   )
   public @ResponseBody
   ResponseEntity<Void> createTaskDefinition(
@@ -84,6 +89,8 @@
   {
     checkProductExists(productIdentifier);
 
+    checkProductChangeable(productIdentifier);
+
     taskDefinitionService.findByIdentifier(productIdentifier, instance.getIdentifier())
             .ifPresent(taskDefinition -> {throw ServiceException.conflict("Duplicate identifier: " + taskDefinition.getIdentifier());});
 
@@ -110,17 +117,19 @@
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
   @RequestMapping(
-          value = "{taskdefinitionidentifier}",
-          method = RequestMethod.PUT,
-          consumes = MediaType.APPLICATION_JSON_VALUE,
-          produces = MediaType.ALL_VALUE
+      value = "{taskdefinitionidentifier}",
+      method = RequestMethod.PUT,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
   )
-  public ResponseEntity<Void> changeTaskDefinition(
+  public @ResponseBody ResponseEntity<Void> changeTaskDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
           @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
           @RequestBody @Valid final TaskDefinition instance)
   {
-    checkProductExists(productIdentifier);
+    checkTaskDefinitionExists(productIdentifier, taskDefinitionIdentifier);
+
+    checkProductChangeable(productIdentifier);
 
     if (!taskDefinitionIdentifier.equals(instance.getIdentifier()))
       throw ServiceException.badRequest("Instance identifiers may not be changed.");
@@ -130,8 +139,40 @@
     return ResponseEntity.accepted().build();
   }
 
-  private void checkProductExists(@PathVariable("productidentifier") String productIdentifier) {
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+  @RequestMapping(
+      value = "/{taskdefinitionidentifier}",
+      method = RequestMethod.DELETE,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public @ResponseBody ResponseEntity<Void> deleteTaskDefinition(
+      @PathVariable("productidentifier") final String productIdentifier,
+      @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier
+  )
+  {
+    checkTaskDefinitionExists(productIdentifier, taskDefinitionIdentifier);
+
+    checkProductChangeable(productIdentifier);
+
+    commandGateway.process(new DeleteTaskDefinitionCommand(productIdentifier, taskDefinitionIdentifier));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  private void checkTaskDefinitionExists(final String productIdentifier,
+                                         final String taskDefinitionIdentifier) {
+    taskDefinitionService.findByIdentifier(productIdentifier, taskDefinitionIdentifier)
+        .orElseThrow(() -> ServiceException.notFound("No task with the identifier ''{0}.{1}'' exists.", productIdentifier, taskDefinitionIdentifier));
+  }
+
+  private void checkProductExists(final String productIdentifier) {
     productService.findByIdentifier(productIdentifier)
             .orElseThrow(() -> ServiceException.notFound("Invalid product referenced."));
   }
+
+  private void checkProductChangeable(final String productIdentifier) {
+    if (caseService.existsByProductIdentifier(productIdentifier))
+      throw ServiceException.conflict("Cases exist for product with the identifier ''{0}''. Product cannot be changed.", productIdentifier);
+  }
 }
\ No newline at end of file
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
index 4493358..6688e4a 100644
--- a/service/src/main/resources/application.yml
+++ b/service/src/main/resources/application.yml
@@ -75,13 +75,4 @@
   threadName: async-processor-
 
 flyway:
-  enabled: false
-
-system:
-  publicKey:
-    exponent: 65537
-    modulus: 21188023007955682867939457181271038457216099278949187456460742046123672432355777599460689470319454021384777684967830053993002724303461144745107517305075315187397862430851722919529943465029389248042840364475999768651348557757734298942211509744303551097953258597691851996692366468761965138767429272032120029271744611798874201312092155969603381492096789028306859853929900848124928201000469425135976322303229632628092728624143573273277870884919055453251617011673264035045823652246768583219018126865521694880333238485410601803458379987829318615730229086183405850999386270584135805252231189505197494383178133769189765423639
-
-portfolio:
-  bookInterestAsUser: interest_user
-  bookInterestInTimeSlot: 0
\ No newline at end of file
+  enabled: false
\ 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 de3c107..f194269 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
@@ -26,7 +26,6 @@
 import io.mifos.portfolio.api.v1.domain.*;
 import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
 import io.mifos.portfolio.service.internal.service.ProductService;
-import io.mifos.portfolio.service.internal.util.ChargeInstance;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,14 +56,6 @@
       this.localDate = localDate;
     }
 
-    Action getAction() {
-      return action;
-    }
-
-    LocalDate getLocalDate() {
-      return localDate;
-    }
-
     @Override
     public boolean equals(Object o) {
       if (this == o) return true;
@@ -97,7 +88,7 @@
     private LocalDate initialDisbursementDate;
     private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
     private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID));
-    private Map<ActionDatePair, List<ChargeInstance>> chargeInstancesForActions = new HashMap<>();
+    private Map<ActionDatePair, List<ChargeDefinition>> chargeDefinitionsForActions = new HashMap<>();
     //This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
     //account designators.  Don't copy the code around charge instances in this test without thinking about what you're
     //doing carefully first.
@@ -138,8 +129,8 @@
 
     TestCase expectChargeInstancesForActionDatePair(final Action action,
                                                     final LocalDate forDate,
-                                                    final List<ChargeInstance> chargeInstances) {
-      this.chargeInstancesForActions.put(new ActionDatePair(action, forDate), chargeInstances);
+                                                    final List<ChargeDefinition> chargeDefinitions) {
+      this.chargeDefinitionsForActions.put(new ActionDatePair(action, forDate), chargeDefinitions);
       return this;
     }
 
@@ -177,13 +168,14 @@
 
     final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
     chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.01, ChronoUnit.YEARS));
-    chargeDefinitionsMappedByAction.put(Action.OPEN.name(),
-            Collections.singletonList(
-                    getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME)));
+    final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
+    chargeDefinitionsMappedByAction.put(Action.OPEN.name(), Collections.singletonList(processingFeeCharge));
+    ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
+    ChargeDefinition loanFundsAllocationCharge = getProportionalSingleChargeDefinition(1.0, Action.APPROVE, LOAN_FUNDS_ALLOCATION_ID, AccountDesignators.LOAN_FUNDS_SOURCE, AccountDesignators.PENDING_DISBURSAL);
     chargeDefinitionsMappedByAction.put(Action.APPROVE.name(),
             Arrays.asList(
-                    getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME),
-                    getProportionalSingleChargeDefinition(1.0, Action.APPROVE, LOAN_FUNDS_ALLOCATION_ID, AccountDesignators.LOAN_FUNDS_SOURCE, AccountDesignators.PENDING_DISBURSAL)));
+                loanOriginationFeeCharge,
+                loanFundsAllocationCharge));
 
     return new TestCase("simpleCase")
             .minorCurrencyUnitDigits(2)
@@ -193,22 +185,9 @@
             .expectAdditionalChargeIdentifier(PROCESSING_FEE_ID)
             .expectAdditionalChargeIdentifier(LOAN_FUNDS_ALLOCATION_ID)
             .expectAdditionalChargeIdentifier(LOAN_ORIGINATION_FEE_ID)
-            .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate,
-                    Collections.singletonList(new ChargeInstance(
-                            AccountDesignators.ENTRY,
-                            AccountDesignators.PROCESSING_FEE_INCOME,
-                            BigDecimal.valueOf(10).setScale(2, BigDecimal.ROUND_UNNECESSARY))))
+            .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
             .expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
-                    Arrays.asList(
-                            new ChargeInstance(
-                                    AccountDesignators.ENTRY,
-                                    AccountDesignators.ORIGINATION_FEE_INCOME,
-                                    BigDecimal.valueOf(100.0).setScale(2, BigDecimal.ROUND_UNNECESSARY)),
-                            new ChargeInstance(
-                                    AccountDesignators.LOAN_FUNDS_SOURCE,
-                                    AccountDesignators.PENDING_DISBURSAL,
-                                    caseParameters.getMaximumBalance().setScale(2, BigDecimal.ROUND_UNNECESSARY)
-                            )));
+                    Arrays.asList(loanOriginationFeeCharge, loanFundsAllocationCharge));
   }
 
   private static TestCase yearLoanTestCase()
@@ -224,7 +203,7 @@
     chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.10, ChronoUnit.YEARS));
 
     return new TestCase("yearLoanTestCase")
-            .minorCurrencyUnitDigits(2)
+            .minorCurrencyUnitDigits(3)
             .caseParameters(caseParameters)
             .initialDisbursementDate(initialDisbursementDate)
             .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction);
@@ -381,6 +360,44 @@
     Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
   }
 
+  @Test
+  public void getScheduledCharges() {
+    final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
+    final List<ScheduledCharge> scheduledCharges = testSubject.getScheduledCharges(testCase.productIdentifier,
+        testCase.minorCurrencyUnitDigits,
+        testCase.caseParameters.getMaximumBalance(),
+        scheduledActions);
+
+    final List<LocalDate> interestCalculationDates = scheduledCharges.stream()
+        .filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.APPLY_INTEREST)
+        .map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
+        .collect(Collectors.toList());
+
+    final List<LocalDate> allTheDaysAfterTheInitialDisbursementDate
+        = Stream.iterate(testCase.initialDisbursementDate.plusDays(1), interestDay -> interestDay.plusDays(1))
+        .limit(interestCalculationDates.size())
+        .collect(Collectors.toList());
+
+    Assert.assertEquals(interestCalculationDates, allTheDaysAfterTheInitialDisbursementDate);
+
+    final List<LocalDate> acceptPaymentDates = scheduledCharges.stream()
+        .filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT)
+        .filter(scheduledCharge -> scheduledCharge.getChargeDefinition().getIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
+        .map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
+        .collect(Collectors.toList());
+    final long expectedAcceptPayments = scheduledActions.stream()
+        .filter(x -> x.action == Action.ACCEPT_PAYMENT).count();
+    Assert.assertEquals("There should be no duplicate entries for payments", expectedAcceptPayments, acceptPaymentDates.size());
+
+    final Map<ActionDatePair, Set<ChargeDefinition>> searchableScheduledCharges = scheduledCharges.stream()
+        .collect(
+            Collectors.groupingBy(scheduledCharge ->
+                new ActionDatePair(scheduledCharge.getScheduledAction().action, scheduledCharge.getScheduledAction().when),
+                Collectors.mapping(ScheduledCharge::getChargeDefinition, Collectors.toSet())));
+
+    testCase.chargeDefinitionsForActions.forEach((key, value) -> Assert.assertEquals(new HashSet<>(value), searchableScheduledCharges.get(key)));
+  }
+
   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);