blob: 21224b3c5fcf6d4ecb9e3aa11f87bdfe8729b89b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 -> {
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 LocalDate startDate = dividendDistributionCommand.dueDate().plusDays(1);
final LocalDate now = LocalDate.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);
}
}