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 <mge...@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 my...@apache.org.