fineract-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From my...@apache.org
Subject [fineract-cn-deposit-account-management] 03/26: added interest calculation
Date Mon, 22 Jan 2018 15:49:25 GMT
This is an automated email from the ASF dual-hosted git repository.

myrle pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract-cn-deposit-account-management.git

commit 333cc52b2083523c7dbcdfd6b0466e3cbe54bb1d
Author: mgeiss <mgeiss@mifos.org>
AuthorDate: Thu Aug 3 17:49:17 2017 +0200

    added interest calculation
---
 .gitignore                                         |   1 +
 .../io/mifos/deposit/api/v1/EventConstants.java    |   7 +
 .../api/v1/client/DepositAccountManager.java       |  25 ++
 .../v1/definition/domain/DividendDistribution.java |  64 +++
 .../v1/definition/domain/ProductDefinition.java    |  24 +-
 .../AbstractDepositAccountManagementTest.java      |   2 +-
 .../src/main/java/io/mifos/deposit/Fixture.java    |   8 +-
 .../main/java/io/mifos/deposit/TestAccrual.java    | 117 ++++++
 .../io/mifos/deposit/TestDividendDistribution.java |  91 ++++
 .../java/io/mifos/deposit/TestProductInstance.java |   1 -
 .../src/main/java/io/mifos/deposit/TestSuite.java  |   4 +-
 .../listener/InterestCalculationEventListener.java |  64 +++
 gradle/wrapper/gradle-wrapper.properties           |   4 +-
 service/build.gradle                               |   8 +-
 .../service/internal/command/AccrualCommand.java   |  27 +-
 .../internal/command/BeatListenerCommand.java      |  27 +-
 .../command/DividendDistributionCommand.java       |  44 ++
 .../internal/command/PayInterestCommand.java       |  27 +-
 .../handler/BeatListenerCommandHandler.java        |  58 +++
 .../command/handler/InterestCalculator.java        | 468 +++++++++++++++++++++
 .../mapper/DividendDistributionMapper.java         |  56 +++
 .../internal/mapper/ProductDefinitionMapper.java   |   4 +
 .../internal/repository/AccruedInterestEntity.java |  75 ++++
 .../repository/AccruedInterestRepository.java      |  22 +-
 .../repository/DividendDistributionEntity.java     | 106 +++++
 .../repository/DividendDistributionRepository.java |  20 +-
 .../repository/ProductDefinitionEntity.java        |  20 +
 .../repository/ProductInstanceRepository.java      |   3 -
 .../internal/service/ProductDefinitionService.java |  21 +-
 .../internal/service/helper/AccountingService.java |  13 +
 .../service/rest/BeatListenerRestController.java   |  67 +++
 .../rest/ProductDefinitionRestController.java      |  52 +++
 .../mariadb/V5__interest_calculation.sql           |  40 ++
 service/src/main/resources/logback.xml             |   8 +-
 shared.gradle                                      |   2 +
 35 files changed, 1496 insertions(+), 84 deletions(-)

diff --git a/.gitignore b/.gitignore
index f9d7cba..1d62734 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 .idea
 build/
 target/
+out/
 
 # Ignore Gradle GUI config
 gradle-app.setting
diff --git a/api/src/main/java/io/mifos/deposit/api/v1/EventConstants.java b/api/src/main/java/io/mifos/deposit/api/v1/EventConstants.java
index 2ebb53b..7e63d55 100644
--- a/api/src/main/java/io/mifos/deposit/api/v1/EventConstants.java
+++ b/api/src/main/java/io/mifos/deposit/api/v1/EventConstants.java
@@ -49,4 +49,11 @@ public interface EventConstants {
 
   String ACTIVATE_PRODUCT_INSTANCE_COMMAND = "ACTIVATE";
   String CLOSE_PRODUCT_INSTANCE_COMMAND = "CLOSE";
+
+  String DIVIDEND_DISTRIBUTION = "dividend-distribution";
+  String SELECTOR_DIVIDEND_DISTRIBUTION = SELECTOR_NAME + " = '" + DIVIDEND_DISTRIBUTION + "'";
+  String INTEREST_ACCRUED = "interest-accrued";
+  String SELECTOR_INTEREST_ACCRUED = SELECTOR_NAME + " = '" + INTEREST_ACCRUED + "'";
+  String INTEREST_PAYED = "interest-payed";
+  String SELECTOR_INTEREST_PAYED = SELECTOR_NAME + " = '" + INTEREST_PAYED + "'";
 }
diff --git a/api/src/main/java/io/mifos/deposit/api/v1/client/DepositAccountManager.java b/api/src/main/java/io/mifos/deposit/api/v1/client/DepositAccountManager.java
index f6ce405..aae77a7 100644
--- a/api/src/main/java/io/mifos/deposit/api/v1/client/DepositAccountManager.java
+++ b/api/src/main/java/io/mifos/deposit/api/v1/client/DepositAccountManager.java
@@ -23,6 +23,7 @@ import io.mifos.deposit.api.v1.definition.ProductDefinitionAlreadyExistsExceptio
 import io.mifos.deposit.api.v1.definition.ProductDefinitionNotFoundException;
 import io.mifos.deposit.api.v1.definition.ProductDefinitionValidationException;
 import io.mifos.deposit.api.v1.definition.domain.Action;
+import io.mifos.deposit.api.v1.definition.domain.DividendDistribution;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinition;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinitionCommand;
 import io.mifos.deposit.api.v1.instance.ProductInstanceNotFoundException;
@@ -188,4 +189,28 @@ public interface DepositAccountManager {
       @ThrowsException(status = HttpStatus.NOT_FOUND, exception = ProductInstanceNotFoundException.class)
   })
   ProductInstance findProductInstance(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/definitions/{identifier}/dividends",
+      method = RequestMethod.POST,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = ProductDefinitionNotFoundException.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = ProductDefinitionValidationException.class)
+  })
+  void dividendDistribution(@PathVariable("identifier") final String identifier,
+                            @RequestBody @Valid final DividendDistribution dividendDistribution);
+
+  @RequestMapping(
+      value = "/definitions/{identifier}/dividends",
+      method = RequestMethod.GET,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.ALL_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.NOT_FOUND, exception = ProductDefinitionNotFoundException.class),
+  })
+  List<DividendDistribution> fetchDividendDistributions(@PathVariable("identifier") final String identifier);
 }
diff --git a/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/DividendDistribution.java b/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/DividendDistribution.java
new file mode 100644
index 0000000..3186deb
--- /dev/null
+++ b/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/DividendDistribution.java
@@ -0,0 +1,64 @@
+/*
+ * 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.deposit.api.v1.definition.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+public class DividendDistribution {
+
+  @NotBlank
+  private String dueDate;
+  @NotBlank
+  private String dividendRate;
+
+  public DividendDistribution() {
+    super();
+  }
+
+  public String getDueDate() {
+    return this.dueDate;
+  }
+
+  public void setDueDate(final String dueDate) {
+    this.dueDate = dueDate;
+  }
+
+  public String getDividendRate() {
+    return this.dividendRate;
+  }
+
+  public void setDividendRate(final String dividendRate) {
+    this.dividendRate = dividendRate;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    final DividendDistribution that = (DividendDistribution) o;
+
+    if (!dueDate.equals(that.dueDate)) return false;
+    return dividendRate.equals(that.dividendRate);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = dueDate.hashCode();
+    result = 31 * result + dividendRate.hashCode();
+    return result;
+  }
+}
diff --git a/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/ProductDefinition.java b/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/ProductDefinition.java
index 05846b8..59d89c3 100644
--- a/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/ProductDefinition.java
+++ b/api/src/main/java/io/mifos/deposit/api/v1/definition/domain/ProductDefinition.java
@@ -36,10 +36,14 @@ public class ProductDefinition {
   private Currency currency;
   @NotNull
   private Double minimumBalance;
-  @ValidIdentifier
+  @ValidIdentifier(maxLength = 34)
   private String equityLedgerIdentifier;
-  @ValidIdentifier
+  @ValidIdentifier(maxLength = 34)
+  private String cashAccountIdentifier;
+  @ValidIdentifier(maxLength = 34)
   private String expenseAccountIdentifier;
+  @ValidIdentifier(maxLength = 34, optional = true)
+  private String accrueAccountIdentifier;
   private Double interest;
   @Valid
   @NotNull
@@ -109,6 +113,14 @@ public class ProductDefinition {
     this.equityLedgerIdentifier = equityLedgerIdentifier;
   }
 
+  public String getCashAccountIdentifier() {
+    return this.cashAccountIdentifier;
+  }
+
+  public void setCashAccountIdentifier(final String cashAccountIdentifier) {
+    this.cashAccountIdentifier = cashAccountIdentifier;
+  }
+
   public String getExpenseAccountIdentifier() {
     return this.expenseAccountIdentifier;
   }
@@ -117,6 +129,14 @@ public class ProductDefinition {
     this.expenseAccountIdentifier = expenseAccountIdentifier;
   }
 
+  public String getAccrueAccountIdentifier() {
+    return this.accrueAccountIdentifier;
+  }
+
+  public void setAccrueAccountIdentifier(final String accrueAccountIdentifier) {
+    this.accrueAccountIdentifier = accrueAccountIdentifier;
+  }
+
   public Double getInterest() {
     return this.interest;
   }
diff --git a/component-test/src/main/java/io/mifos/deposit/AbstractDepositAccountManagementTest.java b/component-test/src/main/java/io/mifos/deposit/AbstractDepositAccountManagementTest.java
index b93a584..6617bc6 100644
--- a/component-test/src/main/java/io/mifos/deposit/AbstractDepositAccountManagementTest.java
+++ b/component-test/src/main/java/io/mifos/deposit/AbstractDepositAccountManagementTest.java
@@ -62,7 +62,7 @@ public abstract class AbstractDepositAccountManagementTest extends SuiteTestEnvi
 
   @Autowired
   @Qualifier(TEST_LOGGER)
-  private Logger logger;
+  protected Logger logger;
 
   @Autowired
   DepositAccountManager depositAccountManager;
diff --git a/component-test/src/main/java/io/mifos/deposit/Fixture.java b/component-test/src/main/java/io/mifos/deposit/Fixture.java
index b4929a1..bf2451d 100644
--- a/component-test/src/main/java/io/mifos/deposit/Fixture.java
+++ b/component-test/src/main/java/io/mifos/deposit/Fixture.java
@@ -62,15 +62,17 @@ public class Fixture {
     currency.setScale(3);
 
     final ProductDefinition productDefinition = new ProductDefinition();
-    productDefinition.setType(Type.SHARE.name());
+    productDefinition.setType(Type.SAVINGS.name());
     productDefinition.setIdentifier(RandomStringUtils.randomAlphanumeric(8));
     productDefinition.setName(RandomStringUtils.randomAlphanumeric(256));
     productDefinition.setDescription(RandomStringUtils.randomAlphanumeric(2048));
     productDefinition.setCharges(new HashSet<>(Arrays.asList(openingCharge, closingCharge)));
     productDefinition.setCurrency(currency);
     productDefinition.setInterest(1.25D);
-    productDefinition.setEquityLedgerIdentifier("20300");
-    productDefinition.setExpenseAccountIdentifier("30300");
+    productDefinition.setEquityLedgerIdentifier("91xx");
+    productDefinition.setCashAccountIdentifier("76xx");
+    productDefinition.setExpenseAccountIdentifier("38xx");
+    productDefinition.setAccrueAccountIdentifier("82xx");
     productDefinition.setFlexible(Boolean.FALSE);
     productDefinition.setMinimumBalance(50.00);
     productDefinition.setTerm(term);
diff --git a/component-test/src/main/java/io/mifos/deposit/TestAccrual.java b/component-test/src/main/java/io/mifos/deposit/TestAccrual.java
new file mode 100644
index 0000000..88d50c5
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/deposit/TestAccrual.java
@@ -0,0 +1,117 @@
+/*
+ * 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.deposit;
+
+import io.mifos.accounting.api.v1.domain.Account;
+import io.mifos.accounting.api.v1.domain.AccountType;
+import io.mifos.core.api.util.ApiFactory;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.deposit.api.v1.EventConstants;
+import io.mifos.deposit.api.v1.definition.domain.ProductDefinition;
+import io.mifos.deposit.api.v1.definition.domain.ProductDefinitionCommand;
+import io.mifos.deposit.api.v1.domain.Type;
+import io.mifos.deposit.api.v1.instance.domain.ProductInstance;
+import io.mifos.deposit.service.internal.repository.AccruedInterestEntity;
+import io.mifos.deposit.service.internal.repository.AccruedInterestRepository;
+import io.mifos.rhythm.spi.v1.client.BeatListener;
+import io.mifos.rhythm.spi.v1.domain.BeatPublish;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+public class TestAccrual extends AbstractDepositAccountManagementTest {
+
+  @Autowired
+  private AccruedInterestRepository accruedInterestRepository;
+
+  private BeatListener depositBeatListener;
+
+  public TestAccrual() {
+    super();
+  }
+
+  @Before
+  public void prepBeatListener() {
+    depositBeatListener = new ApiFactory(super.logger)
+        .create(BeatListener.class, AbstractDepositAccountManagementTest.testEnvironment.serverURI());
+  }
+
+  @Test
+  public void shouldAccrueInterest() throws Exception {
+    final ProductDefinition productDefinition = Fixture.productDefinition();
+    productDefinition.setType(Type.SAVINGS.name());
+    productDefinition.setInterest(2.50D);
+    super.depositAccountManager.create(productDefinition);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_DEFINITION, productDefinition.getIdentifier());
+
+    final ProductDefinitionCommand productDefinitionCommand = new ProductDefinitionCommand();
+    productDefinitionCommand.setAction(ProductDefinitionCommand.Action.ACTIVATE.name());
+    super.depositAccountManager.process(productDefinition.getIdentifier(), productDefinitionCommand);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_DEFINITION_COMMAND, productDefinition.getIdentifier());
+
+    final ProductInstance productInstance = Fixture.productInstance(productDefinition.getIdentifier());
+    super.depositAccountManager.create(productInstance);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_INSTANCE, productInstance.getCustomerIdentifier());
+
+    final List<ProductInstance> productInstances = super.depositAccountManager.findProductInstances(productDefinition.getIdentifier());
+    Assert.assertNotNull(productInstances);
+    Assert.assertEquals(1, productInstances.size());
+    final ProductInstance foundProductInstance = productInstances.get(0);
+
+    super.depositAccountManager.postProductInstanceCommand(
+        foundProductInstance.getAccountIdentifier(), EventConstants.ACTIVATE_PRODUCT_INSTANCE_COMMAND);
+    super.eventRecorder.wait(EventConstants.ACTIVATE_PRODUCT_INSTANCE, foundProductInstance.getAccountIdentifier());
+
+    final Account shareAccount = new Account();
+    shareAccount.setType(AccountType.EQUITY.name());
+    shareAccount.setIdentifier(foundProductInstance.getAccountIdentifier());
+    shareAccount.setBalance(1000.00D);
+
+    Mockito
+        .doAnswer(invocation -> shareAccount)
+        .when(super.accountingServiceSpy).findAccount(shareAccount.getIdentifier());
+
+    final LocalDateTime dueDate = DateConverter.fromIsoString("2017-08-02T22:00:00.000Z");
+    final BeatPublish beatPublish = new BeatPublish();
+    beatPublish.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    beatPublish.setForTime(DateConverter.toIsoString(dueDate));
+    this.depositBeatListener.publishBeat(beatPublish);
+
+    super.eventRecorder.wait(EventConstants.INTEREST_ACCRUED, DateConverter.toIsoString(dueDate.toLocalDate()));
+
+    final Optional<AccruedInterestEntity> optionalAccruedInterest =
+        this.accruedInterestRepository.findByCustomerAccountIdentifier(foundProductInstance.getAccountIdentifier());
+
+    Assert.assertTrue(optionalAccruedInterest.isPresent());
+    final AccruedInterestEntity accruedInterestEntity = optionalAccruedInterest.get();
+    final Double interest =
+        accruedInterestEntity.getAmount()
+            * DateConverter.fromIsoString(beatPublish.getForTime()).toLocalDate().lengthOfYear();
+
+    final Double roundedInterest =
+        BigDecimal.valueOf(interest).setScale(2, BigDecimal.ROUND_HALF_EVEN).doubleValue();
+
+    Assert.assertEquals(25.00D, roundedInterest, 0.00D);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/deposit/TestDividendDistribution.java b/component-test/src/main/java/io/mifos/deposit/TestDividendDistribution.java
new file mode 100644
index 0000000..e6360ed
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/deposit/TestDividendDistribution.java
@@ -0,0 +1,91 @@
+/*
+ * 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.deposit;
+
+import io.mifos.accounting.api.v1.domain.Account;
+import io.mifos.accounting.api.v1.domain.AccountType;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.deposit.api.v1.EventConstants;
+import io.mifos.deposit.api.v1.definition.domain.DividendDistribution;
+import io.mifos.deposit.api.v1.definition.domain.ProductDefinition;
+import io.mifos.deposit.api.v1.definition.domain.ProductDefinitionCommand;
+import io.mifos.deposit.api.v1.domain.Type;
+import io.mifos.deposit.api.v1.instance.domain.ProductInstance;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+public class TestDividendDistribution extends AbstractDepositAccountManagementTest {
+
+  public TestDividendDistribution() {
+    super();
+  }
+
+  @Test
+  public void shouldDistributeDividend() throws Exception {
+    final ProductDefinition productDefinition = Fixture.productDefinition();
+    productDefinition.setType(Type.SHARE.name());
+    productDefinition.setInterest(null);
+    productDefinition.setAccrueAccountIdentifier(null);
+    super.depositAccountManager.create(productDefinition);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_DEFINITION, productDefinition.getIdentifier());
+
+    final ProductDefinitionCommand productDefinitionCommand = new ProductDefinitionCommand();
+    productDefinitionCommand.setAction(ProductDefinitionCommand.Action.ACTIVATE.name());
+    super.depositAccountManager.process(productDefinition.getIdentifier(), productDefinitionCommand);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_DEFINITION_COMMAND, productDefinition.getIdentifier());
+
+    final ProductInstance productInstance = Fixture.productInstance(productDefinition.getIdentifier());
+    super.depositAccountManager.create(productInstance);
+    super.eventRecorder.wait(EventConstants.POST_PRODUCT_INSTANCE, productInstance.getCustomerIdentifier());
+
+    final List<ProductInstance> productInstances = super.depositAccountManager.findProductInstances(productDefinition.getIdentifier());
+    Assert.assertNotNull(productInstances);
+    Assert.assertEquals(1, productInstances.size());
+    final ProductInstance foundProductInstance = productInstances.get(0);
+
+    super.depositAccountManager.postProductInstanceCommand(
+        foundProductInstance.getAccountIdentifier(), EventConstants.ACTIVATE_PRODUCT_INSTANCE_COMMAND);
+    super.eventRecorder.wait(EventConstants.ACTIVATE_PRODUCT_INSTANCE, foundProductInstance.getAccountIdentifier());
+
+    final Account shareAccount = new Account();
+    shareAccount.setType(AccountType.EQUITY.name());
+    shareAccount.setIdentifier(foundProductInstance.getAccountIdentifier());
+    shareAccount.setBalance(1000.00D);
+
+    Mockito
+        .doAnswer(invocation -> shareAccount)
+        .when(super.accountingServiceSpy).findAccount(shareAccount.getIdentifier());
+
+    final DividendDistribution dividendDistribution = new DividendDistribution();
+    dividendDistribution.setDueDate("2017-07-31Z");
+    dividendDistribution.setDividendRate("2.5");
+    this.depositAccountManager.dividendDistribution(productDefinition.getIdentifier(), dividendDistribution);
+
+    Assert.assertTrue(super.eventRecorder.wait(EventConstants.DIVIDEND_DISTRIBUTION, productDefinition.getIdentifier()));
+
+    final List<DividendDistribution> dividendDistributions =
+        super.depositAccountManager.fetchDividendDistributions(productDefinition.getIdentifier());
+
+    Assert.assertEquals(1, dividendDistributions.size());
+    Assert.assertTrue(dividendDistribution.equals(dividendDistributions.get(0)));
+
+    Mockito.verify(super.accountingServiceSpy, Mockito.times(2)).post(Matchers.any(JournalEntry.class));
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/deposit/TestProductInstance.java b/component-test/src/main/java/io/mifos/deposit/TestProductInstance.java
index e867830..be05a00 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestProductInstance.java
+++ b/component-test/src/main/java/io/mifos/deposit/TestProductInstance.java
@@ -30,7 +30,6 @@ import org.mockito.Mockito;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 public class TestProductInstance extends AbstractDepositAccountManagementTest {
 
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/component-test/src/main/java/io/mifos/deposit/TestSuite.java
index abb4cc2..5ffc740 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/component-test/src/main/java/io/mifos/deposit/TestSuite.java
@@ -23,9 +23,11 @@ import org.junit.runners.Suite;
  */
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
-    TestActions.class,
     TestProductDefinition.class,
     TestProductInstance.class,
+    TestActions.class,
+    TestAccrual.class,
+    TestDividendDistribution.class
 })
 public class TestSuite extends SuiteTestEnvironment {
 }
diff --git a/component-test/src/main/java/io/mifos/deposit/listener/InterestCalculationEventListener.java b/component-test/src/main/java/io/mifos/deposit/listener/InterestCalculationEventListener.java
new file mode 100644
index 0000000..6593497
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/deposit/listener/InterestCalculationEventListener.java
@@ -0,0 +1,64 @@
+/*
+ * 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.deposit.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.deposit.AbstractDepositAccountManagementTest;
+import io.mifos.deposit.api.v1.EventConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@Component
+public class InterestCalculationEventListener {
+
+  private final Logger logger;
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public InterestCalculationEventListener(@Qualifier(AbstractDepositAccountManagementTest.TEST_LOGGER) final Logger logger,
+                                          final EventRecorder eventRecorder) {
+    super();
+    this.logger = logger;
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_INTEREST_ACCRUED,
+      subscription = EventConstants.DESTINATION
+  )
+  public void onAccrual(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                        final String payload) {
+    this.logger.debug("Accrual processed, payload: {}.", payload);
+    this.eventRecorder.event(tenant, EventConstants.INTEREST_ACCRUED, payload, String.class);
+  }
+
+  @JmsListener(
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_DIVIDEND_DISTRIBUTION,
+      subscription = EventConstants.DESTINATION
+  )
+  public void onDividendDistribution(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                     final String payload) {
+    this.logger.debug("Dividend distributed for product {}.", payload);
+    this.eventRecorder.event(tenant, EventConstants.DIVIDEND_DISTRIBUTION, payload, String.class);
+  }
+}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2888922..71e5108 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Mar 17 17:54:20 CET 2017
+#Tue Jul 25 13:05:05 CEST 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
diff --git a/service/build.gradle b/service/build.gradle
index bee970e..bf6ef13 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -39,7 +39,13 @@ dependencies {
             [group: 'io.mifos.core', name: 'mariadb', version: versions.frameworkmariadb],
             [group: 'io.mifos.core', name: 'command', version: versions.frameworkcommand],
             [group: 'io.mifos.accounting', name: 'api', version: versions.frameworkledger],
-            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+            [group: 'io.mifos.rhythm', name: 'spi', version: versions.frameworkrhythm],
+            [group: 'io.mifos.rhythm', name: 'api', version: versions.frameworkrhythm],
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
+            [group: 'org.javamoney.lib', name: 'javamoney-calc', version: versions.javamoneylib],
+            [group: 'javax.money', name: 'money-api', version: '1.0.1'],
+            [group: 'org.javamoney', name: 'moneta', version: '1.0.1'],
+            [group: 'org.threeten', name: 'threeten-extra', version: '1.2']
     )
 }
 
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/service/src/main/java/io/mifos/deposit/service/internal/command/AccrualCommand.java
similarity index 66%
copy from component-test/src/main/java/io/mifos/deposit/TestSuite.java
copy to service/src/main/java/io/mifos/deposit/service/internal/command/AccrualCommand.java
index abb4cc2..b306f6f 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/AccrualCommand.java
@@ -13,19 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.deposit;
+package io.mifos.deposit.service.internal.command;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import java.time.LocalDate;
 
-/**
- * @author Myrle Krantz
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    TestActions.class,
-    TestProductDefinition.class,
-    TestProductInstance.class,
-})
-public class TestSuite extends SuiteTestEnvironment {
+public class AccrualCommand {
+
+  private final LocalDate dueDate;
+
+  public AccrualCommand(final LocalDate dueDate) {
+    super();
+    this.dueDate = dueDate;
+  }
+
+  public LocalDate dueDate() {
+    return this.dueDate;
+  }
 }
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/service/src/main/java/io/mifos/deposit/service/internal/command/BeatListenerCommand.java
similarity index 62%
copy from component-test/src/main/java/io/mifos/deposit/TestSuite.java
copy to service/src/main/java/io/mifos/deposit/service/internal/command/BeatListenerCommand.java
index abb4cc2..0f99a93 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/BeatListenerCommand.java
@@ -13,19 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.deposit;
+package io.mifos.deposit.service.internal.command;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import io.mifos.rhythm.spi.v1.domain.BeatPublish;
 
-/**
- * @author Myrle Krantz
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    TestActions.class,
-    TestProductDefinition.class,
-    TestProductInstance.class,
-})
-public class TestSuite extends SuiteTestEnvironment {
+public class BeatListenerCommand {
+
+  private final BeatPublish beatPublish;
+
+  public BeatListenerCommand(final BeatPublish beatPublish) {
+    super();
+    this.beatPublish = beatPublish;
+  }
+
+  public BeatPublish beatPublish() {
+    return this.beatPublish;
+  }
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/command/DividendDistributionCommand.java b/service/src/main/java/io/mifos/deposit/service/internal/command/DividendDistributionCommand.java
new file mode 100644
index 0000000..d993dc7
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/DividendDistributionCommand.java
@@ -0,0 +1,44 @@
+/*
+ * 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.deposit.service.internal.command;
+
+import java.time.LocalDate;
+
+public class DividendDistributionCommand {
+
+  private final String productDefinition;
+  private final LocalDate dueDate;
+  private final Double rate;
+
+  public DividendDistributionCommand(final String productDefinition, final LocalDate dueDate, final Double rate) {
+    super();
+    this.productDefinition = productDefinition;
+    this.dueDate = dueDate;
+    this.rate = rate;
+  }
+
+  public String productDefinition() {
+    return this.productDefinition;
+  }
+
+  public LocalDate dueDate() {
+    return this.dueDate;
+  }
+
+  public Double rate() {
+    return this.rate;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/service/src/main/java/io/mifos/deposit/service/internal/command/PayInterestCommand.java
similarity index 66%
copy from component-test/src/main/java/io/mifos/deposit/TestSuite.java
copy to service/src/main/java/io/mifos/deposit/service/internal/command/PayInterestCommand.java
index abb4cc2..b987190 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/PayInterestCommand.java
@@ -13,19 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.deposit;
+package io.mifos.deposit.service.internal.command;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import java.time.LocalDate;
 
-/**
- * @author Myrle Krantz
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    TestActions.class,
-    TestProductDefinition.class,
-    TestProductInstance.class,
-})
-public class TestSuite extends SuiteTestEnvironment {
+public class PayInterestCommand {
+
+  private final LocalDate date;
+
+  public PayInterestCommand(final LocalDate date) {
+    super();
+    this.date = date;
+  }
+
+  public LocalDate date() {
+    return this.date;
+  }
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/command/handler/BeatListenerCommandHandler.java b/service/src/main/java/io/mifos/deposit/service/internal/command/handler/BeatListenerCommandHandler.java
new file mode 100644
index 0000000..d47c81e
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/handler/BeatListenerCommandHandler.java
@@ -0,0 +1,58 @@
+/*
+ * 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.deposit.service.internal.command.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.domain.CommandCallback;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.deposit.service.internal.command.AccrualCommand;
+import io.mifos.deposit.service.internal.command.BeatListenerCommand;
+import io.mifos.deposit.service.internal.command.PayInterestCommand;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.transaction.Transactional;
+import java.time.LocalDateTime;
+
+@Aggregate
+public class BeatListenerCommandHandler {
+
+  private final CommandGateway commandGateway;
+
+  @Autowired
+  public BeatListenerCommandHandler(final CommandGateway commandGateway) {
+    super();
+    this.commandGateway = commandGateway;
+  }
+
+  @Transactional
+  @CommandHandler
+  public void process(final BeatListenerCommand beatListenerCommand) {
+    try {
+      final LocalDateTime dueDate = DateConverter.fromIsoString(beatListenerCommand.beatPublish().getForTime());
+      final CommandCallback<String> commandCallback =
+          this.commandGateway.process(new AccrualCommand(dueDate.toLocalDate()), String.class);
+
+      final String date = commandCallback.get();
+      this.commandGateway.process(new PayInterestCommand(DateConverter.dateFromIsoString(date)));
+
+    } catch (Exception ex) {
+      throw ServiceException.internalError("Could not handle beat: {0}", ex.getMessage());
+    }
+  }
+}
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/command/handler/InterestCalculator.java b/service/src/main/java/io/mifos/deposit/service/internal/command/handler/InterestCalculator.java
new file mode 100644
index 0000000..4ac279d
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/command/handler/InterestCalculator.java
@@ -0,0 +1,468 @@
+/*
+ * 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.deposit.service.internal.command.handler;
+
+import com.google.common.collect.Sets;
+import io.mifos.accounting.api.v1.domain.Account;
+import io.mifos.accounting.api.v1.domain.AccountEntry;
+import io.mifos.accounting.api.v1.domain.Creditor;
+import io.mifos.accounting.api.v1.domain.Debtor;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.CommandLogLevel;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.deposit.api.v1.EventConstants;
+import io.mifos.deposit.api.v1.domain.InterestPayable;
+import io.mifos.deposit.api.v1.domain.Type;
+import io.mifos.deposit.service.ServiceConstants;
+import io.mifos.deposit.service.internal.command.AccrualCommand;
+import io.mifos.deposit.service.internal.command.DividendDistributionCommand;
+import io.mifos.deposit.service.internal.command.PayInterestCommand;
+import io.mifos.deposit.service.internal.repository.AccruedInterestEntity;
+import io.mifos.deposit.service.internal.repository.AccruedInterestRepository;
+import io.mifos.deposit.service.internal.repository.CurrencyEntity;
+import io.mifos.deposit.service.internal.repository.CurrencyRepository;
+import io.mifos.deposit.service.internal.repository.DividendDistributionEntity;
+import io.mifos.deposit.service.internal.repository.DividendDistributionRepository;
+import io.mifos.deposit.service.internal.repository.ProductDefinitionEntity;
+import io.mifos.deposit.service.internal.repository.ProductDefinitionRepository;
+import io.mifos.deposit.service.internal.repository.ProductInstanceEntity;
+import io.mifos.deposit.service.internal.repository.ProductInstanceRepository;
+import io.mifos.deposit.service.internal.repository.TermEntity;
+import io.mifos.deposit.service.internal.repository.TermRepository;
+import io.mifos.deposit.service.internal.service.helper.AccountingService;
+import org.apache.commons.lang.RandomStringUtils;
+import org.javamoney.calc.banking.AnnualPercentageYield;
+import org.javamoney.calc.common.Rate;
+import org.javamoney.moneta.Money;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.Sort;
+import org.threeten.extra.YearQuarter;
+
+import javax.money.CurrencyUnit;
+import javax.money.Monetary;
+import javax.money.MonetaryAmount;
+import javax.transaction.Transactional;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.time.Clock;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@Aggregate
+public class InterestCalculator {
+
+  public static final String ACTIVE = "ACTIVE";
+  private final Logger logger;
+  private final ProductDefinitionRepository productDefinitionRepository;
+  private final ProductInstanceRepository productInstanceRepository;
+  private final TermRepository termRepository;
+  private final CurrencyRepository currencyRepository;
+  private final AccountingService accountingService;
+  private final AccruedInterestRepository accruedInterestRepository;
+  private final DividendDistributionRepository dividendDistributionRepository;
+
+  @Autowired
+  public InterestCalculator(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                            final ProductDefinitionRepository productDefinitionRepository,
+                            final ProductInstanceRepository productInstanceRepository,
+                            final TermRepository termRepository,
+                            final CurrencyRepository currencyRepository,
+                            final AccountingService accountingService,
+                            final AccruedInterestRepository accruedInterestRepository,
+                            final DividendDistributionRepository dividendDistributionRepository) {
+    super();
+    this.logger = logger;
+    this.productDefinitionRepository = productDefinitionRepository;
+    this.productInstanceRepository = productInstanceRepository;
+    this.termRepository = termRepository;
+    this.currencyRepository = currencyRepository;
+    this.accruedInterestRepository = accruedInterestRepository;
+    this.accountingService = accountingService;
+    this.dividendDistributionRepository = dividendDistributionRepository;
+  }
+
+  @Transactional
+  @CommandHandler(logStart = CommandLogLevel.DEBUG, logFinish =  CommandLogLevel.DEBUG)
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.INTEREST_ACCRUED)
+  public String process(final AccrualCommand accrualCommand) {
+    final LocalDate accrualDate = accrualCommand.dueDate();
+
+    final List<ProductDefinitionEntity> productDefinitions = this.productDefinitionRepository.findAll();
+
+    productDefinitions.forEach(productDefinitionEntity -> {
+      if (this.accruableProduct(productDefinitionEntity)) {
+
+        final ArrayList<Double> accruedValues = new ArrayList<>();
+
+        final TermEntity term = this.termRepository.findByProductDefinition(productDefinitionEntity);
+        final CurrencyEntity currency = this.currencyRepository.findByProductDefinition(productDefinitionEntity);
+        final CurrencyUnit currencyUnit = Monetary.getCurrency(currency.getCode());
+
+        final List<ProductInstanceEntity> productInstances =
+            this.productInstanceRepository.findByProductDefinition(productDefinitionEntity);
+
+        final Money zero = Money.of(0.00D, currencyUnit);
+
+        productInstances.forEach(productInstanceEntity -> {
+          if (productInstanceEntity.getState().equals(ACTIVE)) {
+
+            final Account account = this.accountingService.findAccount(productInstanceEntity.getAccountIdentifier());
+
+            if (account.getBalance() > 0.00D) {
+              final Money balance = Money.of(account.getBalance(), currencyUnit);
+
+              final Rate rate = Rate.of(productDefinitionEntity.getInterest() / 100.00D);
+
+              final MonetaryAmount accruedInterest =
+                  AnnualPercentageYield
+                      .calculate(balance, rate, this.periodOfInterestPayable(term.getInterestPayable()))
+                      .divide(accrualDate.lengthOfYear());
+
+              if (accruedInterest.isGreaterThan(zero)) {
+                final Double doubleValue =
+                    BigDecimal.valueOf(accruedInterest.getNumber().doubleValue())
+                        .setScale(5, BigDecimal.ROUND_HALF_EVEN).doubleValue();
+
+                accruedValues.add(doubleValue);
+
+                final Optional<AccruedInterestEntity> optionalAccruedInterest =
+                    this.accruedInterestRepository.findByCustomerAccountIdentifier(account.getIdentifier());
+                if (optionalAccruedInterest.isPresent()) {
+                  final AccruedInterestEntity accruedInterestEntity = optionalAccruedInterest.get();
+                  accruedInterestEntity.setAmount(accruedInterestEntity.getAmount() + doubleValue);
+                  this.accruedInterestRepository.save(accruedInterestEntity);
+                } else {
+                  final AccruedInterestEntity accruedInterestEntity = new AccruedInterestEntity();
+                  accruedInterestEntity.setAccrueAccountIdentifier(productDefinitionEntity.getAccrueAccountIdentifier());
+                  accruedInterestEntity.setCustomerAccountIdentifier(account.getIdentifier());
+                  accruedInterestEntity.setAmount(doubleValue);
+                  this.accruedInterestRepository.save(accruedInterestEntity);
+                }
+              }
+            }
+          }
+        });
+
+        final String roundedAmount =
+            BigDecimal.valueOf(accruedValues.parallelStream().reduce(0.00D, Double::sum))
+                .setScale(2, BigDecimal.ROUND_HALF_EVEN).toString();
+
+        final JournalEntry cashToAccrueJournalEntry = new JournalEntry();
+        cashToAccrueJournalEntry.setTransactionIdentifier(RandomStringUtils.randomAlphanumeric(32));
+        cashToAccrueJournalEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
+        cashToAccrueJournalEntry.setTransactionType("INTR");
+        cashToAccrueJournalEntry.setClerk(UserContextHolder.checkedGetUser());
+        cashToAccrueJournalEntry.setNote("Daily accrual for product " + productDefinitionEntity.getIdentifier() + ".");
+
+        final Debtor cashDebtor = new Debtor();
+        cashDebtor.setAccountNumber(productDefinitionEntity.getCashAccountIdentifier());
+        cashDebtor.setAmount(roundedAmount);
+        cashToAccrueJournalEntry.setDebtors(Sets.newHashSet(cashDebtor));
+
+        final Creditor accrueCreditor = new Creditor();
+        accrueCreditor.setAccountNumber(productDefinitionEntity.getAccrueAccountIdentifier());
+        accrueCreditor.setAmount(roundedAmount);
+        cashToAccrueJournalEntry.setCreditors(Sets.newHashSet(accrueCreditor));
+
+        this.accountingService.post(cashToAccrueJournalEntry);
+      }
+    });
+
+    return DateConverter.toIsoString(accrualDate);
+  }
+
+  @Transactional
+  @CommandHandler(logStart = CommandLogLevel.DEBUG, logFinish = CommandLogLevel.DEBUG)
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.INTEREST_PAYED)
+  public String process(final PayInterestCommand payInterestCommand) {
+    final List<ProductDefinitionEntity> productDefinitionEntities = this.productDefinitionRepository.findAll();
+
+    productDefinitionEntities.forEach(productDefinitionEntity -> {
+      if (productDefinitionEntity.getActive()
+          && !productDefinitionEntity.getType().equals(Type.SHARE.name())) {
+        final TermEntity term = this.termRepository.findByProductDefinition(productDefinitionEntity);
+        if (this.shouldPayInterest(term.getInterestPayable(), payInterestCommand.date())) {
+          final List<ProductInstanceEntity> productInstanceEntities =
+              this.productInstanceRepository.findByProductDefinition(productDefinitionEntity);
+
+          productInstanceEntities.forEach(productInstanceEntity -> {
+            if (productInstanceEntity.getState().equals(ACTIVE)) {
+              final Optional<AccruedInterestEntity> optionalAccruedInterestEntity =
+                  this.accruedInterestRepository.findByCustomerAccountIdentifier(productInstanceEntity.getAccountIdentifier());
+
+              if (optionalAccruedInterestEntity.isPresent()) {
+                final AccruedInterestEntity accruedInterestEntity = optionalAccruedInterestEntity.get();
+
+                final String roundedAmount =
+                    BigDecimal.valueOf(accruedInterestEntity.getAmount())
+                        .setScale(2, BigDecimal.ROUND_HALF_EVEN).toString();
+
+                final JournalEntry accrueToExpenseJournalEntry = new JournalEntry();
+                accrueToExpenseJournalEntry.setTransactionIdentifier(RandomStringUtils.randomAlphanumeric(32));
+                accrueToExpenseJournalEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
+                accrueToExpenseJournalEntry.setTransactionType("INTR");
+                accrueToExpenseJournalEntry.setClerk(UserContextHolder.checkedGetUser());
+                accrueToExpenseJournalEntry.setNote("Interest paid.");
+
+                final Debtor accrueDebtor = new Debtor();
+                accrueDebtor.setAccountNumber(accruedInterestEntity.getAccrueAccountIdentifier());
+                accrueDebtor.setAmount(roundedAmount);
+                accrueToExpenseJournalEntry.setDebtors(Sets.newHashSet(accrueDebtor));
+
+                final Creditor expenseCreditor = new Creditor();
+                expenseCreditor.setAccountNumber(productDefinitionEntity.getExpenseAccountIdentifier());
+                expenseCreditor.setAmount(roundedAmount);
+                accrueToExpenseJournalEntry.setCreditors(Sets.newHashSet(expenseCreditor));
+
+                this.accruedInterestRepository.delete(accruedInterestEntity);
+
+                this.accountingService.post(accrueToExpenseJournalEntry);
+
+                this.payoutInterest(
+                    productDefinitionEntity.getExpenseAccountIdentifier(),
+                    accruedInterestEntity.getCustomerAccountIdentifier(),
+                    roundedAmount
+                );
+              }
+            }
+          });
+        }
+      }
+    });
+
+    return EventConstants.INTEREST_PAYED;
+  }
+
+  @Transactional
+  @CommandHandler(logStart = CommandLogLevel.DEBUG, logFinish = CommandLogLevel.DEBUG)
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.DIVIDEND_DISTRIBUTION)
+  public String process(final DividendDistributionCommand dividendDistributionCommand) {
+    final Optional<ProductDefinitionEntity> optionalProductDefinition =
+        this.productDefinitionRepository.findByIdentifier(dividendDistributionCommand.productDefinition());
+    if (optionalProductDefinition.isPresent()) {
+      final ProductDefinitionEntity productDefinitionEntity = optionalProductDefinition.get();
+      if (productDefinitionEntity.getActive()) {
+        final Rate rate = Rate.of(dividendDistributionCommand.rate());
+        final TermEntity term = this.termRepository.findByProductDefinition(productDefinitionEntity);
+        final List<String> dateRanges = this.dateRanges(dividendDistributionCommand.dueDate(), term.getInterestPayable());
+
+        final CurrencyEntity currency = this.currencyRepository.findByProductDefinition(productDefinitionEntity);
+        final CurrencyUnit currencyUnit = Monetary.getCurrency(currency.getCode());
+        final List<ProductInstanceEntity> productInstanceEntities =
+            this.productInstanceRepository.findByProductDefinition(productDefinitionEntity);
+        productInstanceEntities.forEach((ProductInstanceEntity productInstanceEntity) -> {
+          if (productInstanceEntity.getState().equals(ACTIVE)) {
+
+            final Account account =
+                this.accountingService.findAccount(productInstanceEntity.getAccountIdentifier());
+
+            final LocalDateTime startDate = dividendDistributionCommand.dueDate().plusDays(1).atStartOfDay();
+            final LocalDateTime now = LocalDateTime.now(Clock.systemUTC());
+
+            final String findCurrentEntries = DateConverter.toIsoString(startDate) + ".." + DateConverter.toIsoString(now);
+            final List<AccountEntry> currentAccountEntries =
+                this.accountingService.fetchEntries(account.getIdentifier(), findCurrentEntries, Sort.Direction.ASC.name());
+
+            final BalanceHolder balanceHolder;
+            if (currentAccountEntries.isEmpty()) {
+              balanceHolder = new BalanceHolder(account.getBalance());
+            } else {
+              final AccountEntry accountEntry = currentAccountEntries.get(0);
+              balanceHolder = new BalanceHolder(accountEntry.getBalance() - accountEntry.getAmount());
+            }
+
+            final DividendHolder dividendHolder = new DividendHolder(currencyUnit);
+            dateRanges.forEach(dateRange -> {
+              final List<AccountEntry> accountEntries =
+                  this.accountingService.fetchEntries(account.getIdentifier(), dateRange, Sort.Direction.DESC.name());
+              if (!accountEntries.isEmpty()) {
+                balanceHolder.setBalance(accountEntries.get(0).getBalance());
+              }
+
+              final Money currentBalance = Money.of(balanceHolder.getBalance(), currencyUnit);
+              dividendHolder.addAmount(
+                  AnnualPercentageYield
+                      .calculate(currentBalance, rate, 12)
+                      .divide(dividendDistributionCommand.dueDate().lengthOfYear()));
+            });
+
+            if (dividendHolder.getAmount().isGreaterThan(Money.of(0.00D, currencyUnit))) {
+
+              final String roundedAmount =
+                  BigDecimal.valueOf(dividendHolder.getAmount().getNumber().doubleValue())
+                      .setScale(2, BigDecimal.ROUND_HALF_EVEN).toString();
+
+              final JournalEntry cashToExpenseJournalEntry = new JournalEntry();
+              cashToExpenseJournalEntry.setTransactionIdentifier(RandomStringUtils.randomAlphanumeric(32));
+              cashToExpenseJournalEntry.setTransactionDate(DateConverter.toIsoString(now));
+              cashToExpenseJournalEntry.setTransactionType("INTR");
+              cashToExpenseJournalEntry.setClerk(UserContextHolder.checkedGetUser());
+              cashToExpenseJournalEntry.setNote("Dividend distribution.");
+
+              final Debtor cashDebtor = new Debtor();
+              cashDebtor.setAccountNumber(productDefinitionEntity.getCashAccountIdentifier());
+              cashDebtor.setAmount(roundedAmount);
+              cashToExpenseJournalEntry.setDebtors(Sets.newHashSet(cashDebtor));
+
+              final Creditor expenseCreditor = new Creditor();
+              expenseCreditor.setAccountNumber(productDefinitionEntity.getExpenseAccountIdentifier());
+              expenseCreditor.setAmount(roundedAmount);
+              cashToExpenseJournalEntry.setCreditors(Sets.newHashSet(expenseCreditor));
+
+              this.accountingService.post(cashToExpenseJournalEntry);
+
+              this.payoutInterest(
+                  productDefinitionEntity.getExpenseAccountIdentifier(),
+                  account.getIdentifier(),
+                  roundedAmount
+              );
+            }
+          }
+        });
+      }
+      final DividendDistributionEntity dividendDistributionEntity = new DividendDistributionEntity();
+
+      dividendDistributionEntity.setProductDefinition(productDefinitionEntity);
+      dividendDistributionEntity.setDueDate(Date.valueOf(dividendDistributionCommand.dueDate()));
+      dividendDistributionEntity.setRate(dividendDistributionCommand.rate());
+      dividendDistributionEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+      dividendDistributionEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+
+      this.dividendDistributionRepository.save(dividendDistributionEntity);
+    }
+
+    return dividendDistributionCommand.productDefinition();
+  }
+
+  private int periodOfInterestPayable(final String interestPayable) {
+    switch (InterestPayable.valueOf(interestPayable)) {
+      case MONTHLY:
+        return 12;
+      case QUARTERLY:
+        return 4;
+      default:
+        return 1;
+    }
+  }
+
+  private boolean shouldPayInterest(final String interestPayable, final LocalDate date) {
+    switch (InterestPayable.valueOf(interestPayable)) {
+      case MONTHLY:
+        return date.equals(date.withDayOfMonth(date.lengthOfMonth()));
+      case QUARTERLY:
+        return date.equals(YearQuarter.from(date).atEndOfQuarter());
+      case ANNUALLY:
+        return date.getDayOfYear() == date.lengthOfYear();
+      default:
+        return false;
+    }
+  }
+
+  private List<String> dateRanges(final LocalDate dueDate, final String interestPayable) {
+    final int pastDays;
+    switch (InterestPayable.valueOf(interestPayable)) {
+      case MONTHLY:
+        pastDays = dueDate.lengthOfMonth();
+        break;
+      case QUARTERLY:
+        pastDays = YearQuarter.from(dueDate).lengthOfQuarter();
+        break;
+      default:
+        pastDays = dueDate.lengthOfYear();
+    }
+
+    return IntStream
+        .range(1, pastDays)
+        .mapToObj(value -> {
+          final LocalDate before = dueDate.minusDays(value);
+          return DateConverter.toIsoString(before) + ".." + DateConverter.toIsoString(dueDate.minusDays(value - 1));
+        }).collect(Collectors.toList());
+  }
+
+  private class BalanceHolder {
+    private Double balance;
+
+    private BalanceHolder(final Double balance) {
+      super();
+      this.balance = balance;
+    }
+
+    private Double getBalance() {
+      return this.balance;
+    }
+
+    private void setBalance(final Double balance) {
+      this.balance = balance;
+    }
+  }
+
+  private class DividendHolder {
+    private MonetaryAmount amount;
+
+    private DividendHolder(final CurrencyUnit currencyUnit) {
+      super();
+      this.amount = Money.of(0.00D, currencyUnit);
+    }
+
+    private void addAmount(final MonetaryAmount toAdd) {
+      this.amount = this.amount.add(toAdd);
+    }
+
+    private MonetaryAmount getAmount() {
+      return this.amount;
+    }
+  }
+
+  private boolean accruableProduct(final ProductDefinitionEntity productDefinitionEntity) {
+    return productDefinitionEntity.getActive()
+        && !productDefinitionEntity.getType().equals(Type.SHARE.name())
+        && productDefinitionEntity.getInterest() != null
+        && productDefinitionEntity.getInterest() > 0.00D;
+  }
+
+  private void payoutInterest(final String expenseAccount, final String customerAccount, final String amount) {
+    final JournalEntry expenseToCustomerJournalEntry = new JournalEntry();
+    expenseToCustomerJournalEntry.setTransactionIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    expenseToCustomerJournalEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
+    expenseToCustomerJournalEntry.setTransactionType("INTR");
+    expenseToCustomerJournalEntry.setClerk(UserContextHolder.checkedGetUser());
+    expenseToCustomerJournalEntry.setNote("Interest paid.");
+
+    final Debtor expenseDebtor = new Debtor();
+    expenseDebtor.setAccountNumber(expenseAccount);
+    expenseDebtor.setAmount(amount);
+    expenseToCustomerJournalEntry.setDebtors(Sets.newHashSet(expenseDebtor));
+
+    final Creditor customerCreditor = new Creditor();
+    customerCreditor.setAccountNumber(customerAccount);
+    customerCreditor.setAmount(amount);
+    expenseToCustomerJournalEntry.setCreditors(Sets.newHashSet(customerCreditor));
+
+    this.accountingService.post(expenseToCustomerJournalEntry);
+
+  }
+}
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/mapper/DividendDistributionMapper.java b/service/src/main/java/io/mifos/deposit/service/internal/mapper/DividendDistributionMapper.java
new file mode 100644
index 0000000..7c01a8f
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/mapper/DividendDistributionMapper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.deposit.service.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.deposit.api.v1.definition.domain.DividendDistribution;
+import io.mifos.deposit.service.internal.repository.DividendDistributionEntity;
+import io.mifos.deposit.service.internal.repository.ProductDefinitionEntity;
+
+import java.sql.Date;
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public class DividendDistributionMapper {
+
+  private DividendDistributionMapper() {
+    super();
+  }
+
+  public static DividendDistributionEntity map(final DividendDistribution dividendDistribution,
+                                               final ProductDefinitionEntity productDefinitionEntity) {
+    final DividendDistributionEntity dividendDistributionEntity = new DividendDistributionEntity();
+
+    dividendDistributionEntity.setProductDefinition(productDefinitionEntity);
+    final Date dueDate = Date.valueOf(DateConverter.dateFromIsoString(dividendDistribution.getDueDate()));
+    dividendDistributionEntity.setDueDate(dueDate);
+    dividendDistributionEntity.setRate(Double.valueOf(dividendDistribution.getDividendRate()));
+    dividendDistributionEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    dividendDistributionEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+
+    return dividendDistributionEntity;
+  }
+
+  public static DividendDistribution map(final DividendDistributionEntity dividendDistributionEntity) {
+    final DividendDistribution dividendDistribution = new DividendDistribution();
+
+    dividendDistribution.setDividendRate(dividendDistributionEntity.getRate().toString());
+    dividendDistribution.setDueDate(DateConverter.toIsoString(dividendDistributionEntity.getDueDate().toLocalDate()));
+
+    return dividendDistribution;
+  }
+}
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/mapper/ProductDefinitionMapper.java b/service/src/main/java/io/mifos/deposit/service/internal/mapper/ProductDefinitionMapper.java
index 03165a7..f9fccd9 100644
--- a/service/src/main/java/io/mifos/deposit/service/internal/mapper/ProductDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/mapper/ProductDefinitionMapper.java
@@ -32,7 +32,9 @@ public class ProductDefinitionMapper {
     productDefinitionEntity.setDescription(productDefinition.getName());
     productDefinitionEntity.setMinimumBalance(productDefinition.getMinimumBalance());
     productDefinitionEntity.setEquityLedgerIdentifier(productDefinition.getEquityLedgerIdentifier());
+    productDefinitionEntity.setCashAccountIdentifier(productDefinition.getCashAccountIdentifier());
     productDefinitionEntity.setExpenseAccountIdentifier(productDefinition.getExpenseAccountIdentifier());
+    productDefinitionEntity.setAccrueAccountIdentifier(productDefinition.getAccrueAccountIdentifier());
     productDefinitionEntity.setInterest(productDefinition.getInterest());
     productDefinitionEntity.setFlexible(productDefinition.getFlexible());
 
@@ -47,7 +49,9 @@ public class ProductDefinitionMapper {
     productDefinition.setDescription(productDefinitionEntity.getName());
     productDefinition.setMinimumBalance(productDefinitionEntity.getMinimumBalance());
     productDefinition.setEquityLedgerIdentifier(productDefinitionEntity.getEquityLedgerIdentifier());
+    productDefinition.setCashAccountIdentifier(productDefinitionEntity.getCashAccountIdentifier());
     productDefinition.setExpenseAccountIdentifier(productDefinitionEntity.getExpenseAccountIdentifier());
+    productDefinition.setAccrueAccountIdentifier(productDefinitionEntity.getAccrueAccountIdentifier());
     productDefinition.setInterest(productDefinitionEntity.getInterest());
     productDefinition.setFlexible(productDefinitionEntity.getFlexible());
     productDefinition.setActive(productDefinitionEntity.getActive());
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestEntity.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestEntity.java
new file mode 100644
index 0000000..dd0a60d
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestEntity.java
@@ -0,0 +1,75 @@
+/*
+ * 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.deposit.service.internal.repository;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "shed_accrued_interests")
+public class AccruedInterestEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "accrue_account_identifier", nullable = false)
+  private String accrueAccountIdentifier;
+  @Column(name = "customer_account_identifier", nullable = false)
+  private String customerAccountIdentifier;
+  @Column(name = "amount", nullable = false)
+  private Double amount;
+
+  public AccruedInterestEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getAccrueAccountIdentifier() {
+    return this.accrueAccountIdentifier;
+  }
+
+  public void setAccrueAccountIdentifier(final String accrueAccountIdentifier) {
+    this.accrueAccountIdentifier = accrueAccountIdentifier;
+  }
+
+  public String getCustomerAccountIdentifier() {
+    return this.customerAccountIdentifier;
+  }
+
+  public void setCustomerAccountIdentifier(final String customerAccountIdentifier) {
+    this.customerAccountIdentifier = customerAccountIdentifier;
+  }
+
+  public Double getAmount() {
+    return this.amount;
+  }
+
+  public void setAmount(final Double amount) {
+    this.amount = amount;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestRepository.java
similarity index 59%
copy from component-test/src/main/java/io/mifos/deposit/TestSuite.java
copy to service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestRepository.java
index abb4cc2..1a12de1 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/AccruedInterestRepository.java
@@ -13,19 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.deposit;
+package io.mifos.deposit.service.internal.repository;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
 
-/**
- * @author Myrle Krantz
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    TestActions.class,
-    TestProductDefinition.class,
-    TestProductInstance.class,
-})
-public class TestSuite extends SuiteTestEnvironment {
+import java.util.Optional;
+
+@Repository
+public interface AccruedInterestRepository extends JpaRepository<AccruedInterestEntity, Long> {
+
+  Optional<AccruedInterestEntity> findByCustomerAccountIdentifier(final String customerAccountIdentifier);
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionEntity.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionEntity.java
new file mode 100644
index 0000000..384e09e
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionEntity.java
@@ -0,0 +1,106 @@
+/*
+ * 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.deposit.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.sql.Date;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "shed_dividend_distributions")
+public class DividendDistributionEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @ManyToOne(fetch = FetchType.EAGER, optional = false, cascade = CascadeType.ALL)
+  @JoinColumn(name = "product_definition_id", nullable = false)
+  private ProductDefinitionEntity productDefinition;
+  @Column(name = "due_date")
+  private Date dueDate;
+  @Column(name = "rate")
+  private Double rate;
+  @Column(name = "created_by", nullable = false, length = 32)
+  private String createdBy;
+  @Convert(converter = LocalDateTimeConverter.class)
+  @Column(name = "created_on", nullable = false)
+  private LocalDateTime createdOn;
+
+  public DividendDistributionEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public ProductDefinitionEntity getProductDefinition() {
+    return this.productDefinition;
+  }
+
+  public void setProductDefinition(final ProductDefinitionEntity productDefinition) {
+    this.productDefinition = productDefinition;
+  }
+
+  public Date getDueDate() {
+    return this.dueDate;
+  }
+
+  public void setDueDate(final Date dueDate) {
+    this.dueDate = dueDate;
+  }
+
+  public Double getRate() {
+    return this.rate;
+  }
+
+  public void setRate(final Double rate) {
+    this.rate = rate;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/deposit/TestSuite.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionRepository.java
similarity index 61%
copy from component-test/src/main/java/io/mifos/deposit/TestSuite.java
copy to service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionRepository.java
index abb4cc2..754c7c4 100644
--- a/component-test/src/main/java/io/mifos/deposit/TestSuite.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/DividendDistributionRepository.java
@@ -13,19 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.mifos.deposit;
+package io.mifos.deposit.service.internal.repository;
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.springframework.data.jpa.repository.JpaRepository;
 
-/**
- * @author Myrle Krantz
- */
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    TestActions.class,
-    TestProductDefinition.class,
-    TestProductInstance.class,
-})
-public class TestSuite extends SuiteTestEnvironment {
+import java.util.List;
+
+public interface DividendDistributionRepository extends JpaRepository<DividendDistributionEntity, Long> {
+
+  List<DividendDistributionEntity> findByProductDefinitionOrderByDueDateAsc(final ProductDefinitionEntity productDefinitionEntity);
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductDefinitionEntity.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductDefinitionEntity.java
index 802b067..37b5e93 100644
--- a/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductDefinitionEntity.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductDefinitionEntity.java
@@ -46,8 +46,12 @@ public class ProductDefinitionEntity {
   private Double minimumBalance;
   @Column(name = "equity_ledger_identifier", nullable = false)
   private String equityLedgerIdentifier;
+  @Column(name = "cash_account_identifier", nullable = false)
+  private String cashAccountIdentifier;
   @Column(name = "expense_account_identifier", nullable = false)
   private String expenseAccountIdentifier;
+  @Column(name = "accrue_account_identifier", nullable = true)
+  private String accrueAccountIdentifier;
   @Column(name = "interest", nullable = true)
   private Double interest;
   @Column(name = "is_flexible", nullable = false)
@@ -125,6 +129,14 @@ public class ProductDefinitionEntity {
     this.equityLedgerIdentifier = equityLedgerIdentifier;
   }
 
+  public String getCashAccountIdentifier() {
+    return this.cashAccountIdentifier;
+  }
+
+  public void setCashAccountIdentifier(final String cashAccountIdentifier) {
+    this.cashAccountIdentifier = cashAccountIdentifier;
+  }
+
   public String getExpenseAccountIdentifier() {
     return this.expenseAccountIdentifier;
   }
@@ -133,6 +145,14 @@ public class ProductDefinitionEntity {
     this.expenseAccountIdentifier = expenseAccountIdentifier;
   }
 
+  public String getAccrueAccountIdentifier() {
+    return this.accrueAccountIdentifier;
+  }
+
+  public void setAccrueAccountIdentifier(final String accrueAccountIdentifier) {
+    this.accrueAccountIdentifier = accrueAccountIdentifier;
+  }
+
   public Double getInterest() {
     return this.interest;
   }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductInstanceRepository.java b/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductInstanceRepository.java
index 0091204..cad0e15 100644
--- a/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductInstanceRepository.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/repository/ProductInstanceRepository.java
@@ -26,8 +26,5 @@ public interface ProductInstanceRepository extends JpaRepository<ProductInstance
 
   List<ProductInstanceEntity> findByProductDefinition(final ProductDefinitionEntity productDefinitionEntity);
 
-  List<ProductInstanceEntity> findByProductDefinitionAndCustomerIdentifier(
-      final ProductDefinitionEntity productDefinitionEntity, final String customerIdentifier);
-
   Optional<ProductInstanceEntity> findByAccountIdentifier(final String identifier);
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/service/ProductDefinitionService.java b/service/src/main/java/io/mifos/deposit/service/internal/service/ProductDefinitionService.java
index 7430850..53d394e 100644
--- a/service/src/main/java/io/mifos/deposit/service/internal/service/ProductDefinitionService.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/service/ProductDefinitionService.java
@@ -15,17 +15,21 @@
  */
 package io.mifos.deposit.service.internal.service;
 
+import io.mifos.core.lang.ServiceException;
+import io.mifos.deposit.api.v1.definition.domain.DividendDistribution;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinition;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinitionCommand;
 import io.mifos.deposit.service.ServiceConstants;
 import io.mifos.deposit.service.internal.mapper.ChargeMapper;
 import io.mifos.deposit.service.internal.mapper.CurrencyMapper;
+import io.mifos.deposit.service.internal.mapper.DividendDistributionMapper;
 import io.mifos.deposit.service.internal.mapper.ProductDefinitionCommandMapper;
 import io.mifos.deposit.service.internal.mapper.ProductDefinitionMapper;
 import io.mifos.deposit.service.internal.mapper.TermMapper;
 import io.mifos.deposit.service.internal.repository.ActionRepository;
 import io.mifos.deposit.service.internal.repository.ChargeRepository;
 import io.mifos.deposit.service.internal.repository.CurrencyRepository;
+import io.mifos.deposit.service.internal.repository.DividendDistributionRepository;
 import io.mifos.deposit.service.internal.repository.ProductDefinitionCommandRepository;
 import io.mifos.deposit.service.internal.repository.ProductDefinitionEntity;
 import io.mifos.deposit.service.internal.repository.ProductDefinitionRepository;
@@ -50,6 +54,7 @@ public class ProductDefinitionService {
   private final ChargeRepository chargeRepository;
   private final CurrencyRepository currencyRepository;
   private final TermRepository termRepository;
+  private final DividendDistributionRepository dividendDistributionRepository;
 
   @Autowired
   public ProductDefinitionService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
@@ -58,7 +63,8 @@ public class ProductDefinitionService {
                                   final ActionRepository actionRepository,
                                   final ChargeRepository chargeRepository,
                                   final CurrencyRepository currencyRepository,
-                                  final TermRepository termRepository) {
+                                  final TermRepository termRepository,
+                                  final DividendDistributionRepository dividendDistributionRepository) {
     super();
     this.logger = logger;
     this.productDefinitionRepository = productDefinitionRepository;
@@ -67,6 +73,7 @@ public class ProductDefinitionService {
     this.chargeRepository = chargeRepository;
     this.currencyRepository = currencyRepository;
     this.termRepository = termRepository;
+    this.dividendDistributionRepository = dividendDistributionRepository;
   }
 
   public List<ProductDefinition> fetchProductDefinitions() {
@@ -106,4 +113,16 @@ public class ProductDefinitionService {
             .collect(Collectors.toList()))
         .orElseGet(Collections::emptyList);
   }
+
+  public List<DividendDistribution> fetchDividendDistributions(final String identifier) {
+    final Optional<ProductDefinitionEntity> optionalProductDefinition =
+        this.productDefinitionRepository.findByIdentifier(identifier);
+    if (optionalProductDefinition.isPresent()) {
+      return this.dividendDistributionRepository.findByProductDefinitionOrderByDueDateAsc(optionalProductDefinition.get())
+          .stream().map(DividendDistributionMapper::map)
+          .collect(Collectors.toList());
+    } else {
+      throw ServiceException.notFound("Product definition {0} not found", identifier);
+    }
+  }
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/internal/service/helper/AccountingService.java b/service/src/main/java/io/mifos/deposit/service/internal/service/helper/AccountingService.java
index 6110e23..2229a03 100644
--- a/service/src/main/java/io/mifos/deposit/service/internal/service/helper/AccountingService.java
+++ b/service/src/main/java/io/mifos/deposit/service/internal/service/helper/AccountingService.java
@@ -19,6 +19,8 @@ import io.mifos.accounting.api.v1.client.AccountNotFoundException;
 import io.mifos.accounting.api.v1.client.LedgerManager;
 import io.mifos.accounting.api.v1.client.LedgerNotFoundException;
 import io.mifos.accounting.api.v1.domain.Account;
+import io.mifos.accounting.api.v1.domain.AccountEntry;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
 import io.mifos.accounting.api.v1.domain.Ledger;
 import io.mifos.core.lang.ServiceException;
 import io.mifos.deposit.service.ServiceConstants;
@@ -32,6 +34,7 @@ import org.springframework.stereotype.Service;
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 
 @Service
 public class AccountingService {
@@ -86,4 +89,14 @@ public class AccountingService {
   public void updateAccount(final Account account) {
     this.ledgerManager.modifyAccount(account.getIdentifier(), account);
   }
+
+  public List<AccountEntry> fetchEntries(final String identifier, final String dateRange, final String direction) {
+    return this.ledgerManager
+        .fetchAccountEntries(identifier, dateRange, null, 0, 1, "transactionDate", direction)
+        .getAccountEntries();
+  }
+
+  public void post(final JournalEntry journalEntry) {
+    this.ledgerManager.createJournalEntry(journalEntry);
+  }
 }
diff --git a/service/src/main/java/io/mifos/deposit/service/rest/BeatListenerRestController.java b/service/src/main/java/io/mifos/deposit/service/rest/BeatListenerRestController.java
new file mode 100644
index 0000000..61a586f
--- /dev/null
+++ b/service/src/main/java/io/mifos/deposit/service/rest/BeatListenerRestController.java
@@ -0,0 +1,67 @@
+/*
+ * 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.deposit.service.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.deposit.service.ServiceConstants;
+import io.mifos.deposit.service.internal.command.BeatListenerCommand;
+import io.mifos.rhythm.spi.v1.client.BeatListener;
+import io.mifos.rhythm.spi.v1.domain.BeatPublish;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@RestController
+@RequestMapping(BeatListener.PUBLISH_BEAT_PATH)
+public class BeatListenerRestController {
+
+  private final static String BEAT_PUBLISH_PERMISSION = "deposit__v1__khepri";
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+
+  @Autowired
+  public BeatListenerRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                                    final CommandGateway commandGateway) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = BEAT_PUBLISH_PERMISSION)
+  @RequestMapping(
+      method = RequestMethod.POST,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public @ResponseBody
+  ResponseEntity<Void> publishBeat(@RequestBody @Valid final BeatPublish beatPublish)
+  {
+    this.commandGateway.process(new BeatListenerCommand(beatPublish));
+    return ResponseEntity.accepted().build();
+  }
+}
diff --git a/service/src/main/java/io/mifos/deposit/service/rest/ProductDefinitionRestController.java b/service/src/main/java/io/mifos/deposit/service/rest/ProductDefinitionRestController.java
index 05a6a43..7c8f875 100644
--- a/service/src/main/java/io/mifos/deposit/service/rest/ProductDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/deposit/service/rest/ProductDefinitionRestController.java
@@ -18,16 +18,20 @@ package io.mifos.deposit.service.rest;
 import io.mifos.anubis.annotation.AcceptedTokenType;
 import io.mifos.anubis.annotation.Permittable;
 import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.DateConverter;
 import io.mifos.core.lang.ServiceException;
 import io.mifos.deposit.api.v1.PermittableGroupIds;
+import io.mifos.deposit.api.v1.definition.domain.DividendDistribution;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinition;
 import io.mifos.deposit.api.v1.definition.domain.ProductDefinitionCommand;
+import io.mifos.deposit.api.v1.domain.Type;
 import io.mifos.deposit.api.v1.instance.domain.ProductInstance;
 import io.mifos.deposit.service.ServiceConstants;
 import io.mifos.deposit.service.internal.command.ActivateProductDefinitionCommand;
 import io.mifos.deposit.service.internal.command.CreateProductDefinitionCommand;
 import io.mifos.deposit.service.internal.command.DeactivateProductDefinitionCommand;
 import io.mifos.deposit.service.internal.command.DeleteProductDefinitionCommand;
+import io.mifos.deposit.service.internal.command.DividendDistributionCommand;
 import io.mifos.deposit.service.internal.command.UpdateProductDefinitionCommand;
 import io.mifos.deposit.service.internal.service.ProductDefinitionService;
 import io.mifos.deposit.service.internal.service.ProductInstanceService;
@@ -44,6 +48,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.validation.Valid;
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Optional;
 
@@ -77,6 +82,11 @@ public class ProductDefinitionRestController {
   )
   @ResponseBody
   public ResponseEntity<Void> create(@RequestBody @Valid final ProductDefinition productDefinition) {
+    if (!productDefinition.getType().equals(Type.SHARE.name())
+        && productDefinition.getAccrueAccountIdentifier() == null) {
+      throw ServiceException.badRequest("Accrue account must be given.");
+    }
+
     if (this.productDefinitionService.findProductDefinition(productDefinition.getIdentifier()).isPresent()) {
       throw ServiceException.conflict("Product definition{0} already exists.", productDefinition.getIdentifier());
     } else {
@@ -227,4 +237,46 @@ public class ProductDefinitionRestController {
 
     return ResponseEntity.accepted().build();
   }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DEFINITION_MANAGEMENT)
+  @RequestMapping(
+      value = "/{identifier}/dividends",
+      method = RequestMethod.POST,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ResponseBody
+  ResponseEntity<Void> dividendDistribution(@PathVariable("identifier") final String identifier,
+                                            @RequestBody @Valid final DividendDistribution dividendDistribution) {
+    final Optional<ProductDefinition> optionalProductDefinition = this.productDefinitionService.findProductDefinition(identifier);
+    if (!optionalProductDefinition.isPresent()) {
+      throw ServiceException.notFound("Product definition {0} not found", identifier);
+    } else {
+      final ProductDefinition productDefinition = optionalProductDefinition.get();
+      if (!productDefinition.getType().equals(Type.SHARE.name())) {
+        throw ServiceException.badRequest("Product definition {0} is not a share product.", identifier);
+      }
+    }
+
+    final LocalDate dueDate = DateConverter.dateFromIsoString(dividendDistribution.getDueDate());
+    final Double amount = Double.valueOf(dividendDistribution.getDividendRate());
+
+    this.commandGateway.process(new DividendDistributionCommand(identifier, dueDate, amount));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.DEFINITION_MANAGEMENT)
+  @RequestMapping(
+      value = "/{identifier}/dividends",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ResponseBody
+  ResponseEntity<List<DividendDistribution>> fetchDividendDistributions(@PathVariable("identifier") final String identifier) {
+    return ResponseEntity.ok(
+        this.productDefinitionService.fetchDividendDistributions(identifier)
+    );
+  }
 }
diff --git a/service/src/main/resources/db/migrations/mariadb/V5__interest_calculation.sql b/service/src/main/resources/db/migrations/mariadb/V5__interest_calculation.sql
new file mode 100644
index 0000000..786b24a
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V5__interest_calculation.sql
@@ -0,0 +1,40 @@
+--
+-- Copyright 2017 The Mifos Initiative.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--    http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+ALTER TABLE shed_product_definitions ADD COLUMN cash_account_identifier VARCHAR(34) NULL;
+ALTER TABLE shed_product_definitions ADD COLUMN accrue_account_identifier VARCHAR(34) NULL;
+ALTER TABLE shed_product_definitions MODIFY COLUMN equity_ledger_identifier VARCHAR(34) NOT NULL;
+ALTER TABLE shed_product_definitions MODIFY COLUMN expense_account_identifier VARCHAR(34) NOT NULL;
+
+CREATE TABLE shed_accrued_interests (
+  id                          BIGINT         NOT NULL AUTO_INCREMENT,
+  accrue_account_identifier   VARCHAR(34)    NOT NULL,
+  customer_account_identifier VARCHAR(34)    NOT NULL,
+  amount                      NUMERIC(15, 5) NOT NULL,
+  CONSTRAINT shed_accrued_interests_pk PRIMARY KEY (id),
+  CONSTRAINT shed_accrued_interests_uq UNIQUE (accrue_account_identifier, customer_account_identifier)
+);
+
+CREATE TABLE shed_dividend_distributions (
+  id                    BIGINT         NOT NULL AUTO_INCREMENT,
+  product_definition_id BIGINT         NOT NULL,
+  due_date              DATE           NOT NULL,
+  rate                  NUMERIC(15, 5) NOT NULL,
+  created_on            TIMESTAMP(3)   NOT NULL,
+  created_by            VARCHAR(32)    NOT NULL,
+  CONSTRAINT shed_dividend_distributions PRIMARY KEY (id),
+  CONSTRAINT shed_div_dist_prod_def_fk FOREIGN KEY (product_definition_id) REFERENCES shed_product_definitions (id)
+);
diff --git a/service/src/main/resources/logback.xml b/service/src/main/resources/logback.xml
index 25305f9..194618e 100644
--- a/service/src/main/resources/logback.xml
+++ b/service/src/main/resources/logback.xml
@@ -33,11 +33,11 @@
         </encoder>
     </appender>
 
-    <logger name="com" level="WARN">
+    <logger name="com" level="INFO">
         <appender-ref ref="STDOUT" />
     </logger>
 
-    <logger name="org" level="WARN">
+    <logger name="org" level="INFO">
         <appender-ref ref="STDOUT" />
     </logger>
 
@@ -45,11 +45,11 @@
         <appender-ref ref="STDOUT" />
     </logger>
 
-    <logger name="net" level="WARN">
+    <logger name="net" level="INFO">
         <appender-ref ref="STDOUT" />
     </logger>
 
-    <root level="INFO">
+    <root level="DEBUG">
         <appender-ref ref="FILE"/>
     </root>
 </configuration>
\ No newline at end of file
diff --git a/shared.gradle b/shared.gradle
index 8d20f5f..a456f78 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -11,6 +11,8 @@ ext.versions = [
         frameworktest     : '0.1.0-BUILD-SNAPSHOT',
         frameworkanubis   : '0.1.0-BUILD-SNAPSHOT',
         frameworkledger   : '0.1.0-BUILD-SNAPSHOT',
+        frameworkrhythm   : '0.1.0-BUILD-SNAPSHOT',
+        javamoneylib      : '0.9-SNAPSHOT',
         validator         : '5.3.0.Final'
 ]
 

-- 
To stop receiving notification emails like this one, please contact
myrle@apache.org.

Mime
View raw message