blob: 79dcc6047c50a40fe5216d4a43ca202a5f578118 [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 org.apache.fineract.cn.deposit.service.internal.command.handler;
import com.google.common.collect.Sets;
import java.math.BigDecimal;
import java.math.MathContext;
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;
import javax.transaction.Transactional;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.fineract.cn.accounting.api.v1.domain.Account;
import org.apache.fineract.cn.accounting.api.v1.domain.AccountEntry;
import org.apache.fineract.cn.accounting.api.v1.domain.Creditor;
import org.apache.fineract.cn.accounting.api.v1.domain.Debtor;
import org.apache.fineract.cn.accounting.api.v1.domain.JournalEntry;
import org.apache.fineract.cn.api.util.UserContextHolder;
import org.apache.fineract.cn.command.annotation.Aggregate;
import org.apache.fineract.cn.command.annotation.CommandHandler;
import org.apache.fineract.cn.command.annotation.CommandLogLevel;
import org.apache.fineract.cn.command.annotation.EventEmitter;
import org.apache.fineract.cn.deposit.api.v1.EventConstants;
import org.apache.fineract.cn.deposit.api.v1.domain.InterestPayable;
import org.apache.fineract.cn.deposit.api.v1.domain.Type;
import org.apache.fineract.cn.deposit.service.internal.command.AccrualCommand;
import org.apache.fineract.cn.deposit.service.internal.command.DividendDistributionCommand;
import org.apache.fineract.cn.deposit.service.internal.command.PayInterestCommand;
import org.apache.fineract.cn.deposit.service.internal.repository.AccruedInterestEntity;
import org.apache.fineract.cn.deposit.service.internal.repository.AccruedInterestRepository;
import org.apache.fineract.cn.deposit.service.internal.repository.DividendDistributionEntity;
import org.apache.fineract.cn.deposit.service.internal.repository.DividendDistributionRepository;
import org.apache.fineract.cn.deposit.service.internal.repository.ProductDefinitionEntity;
import org.apache.fineract.cn.deposit.service.internal.repository.ProductDefinitionRepository;
import org.apache.fineract.cn.deposit.service.internal.repository.ProductInstanceEntity;
import org.apache.fineract.cn.deposit.service.internal.repository.ProductInstanceRepository;
import org.apache.fineract.cn.deposit.service.internal.repository.TermEntity;
import org.apache.fineract.cn.deposit.service.internal.repository.TermRepository;
import org.apache.fineract.cn.deposit.service.internal.service.helper.AccountingService;
import org.apache.fineract.cn.lang.DateConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.threeten.extra.YearQuarter;
@Aggregate
public class InterestCalculator {
private static final int INTEREST_PRECISION = 7;
private static final String ACTIVE = "ACTIVE";
private final ProductDefinitionRepository productDefinitionRepository;
private final ProductInstanceRepository productInstanceRepository;
private final TermRepository termRepository;
private final AccountingService accountingService;
private final AccruedInterestRepository accruedInterestRepository;
private final DividendDistributionRepository dividendDistributionRepository;
@Autowired
public InterestCalculator(
final ProductDefinitionRepository productDefinitionRepository,
final ProductInstanceRepository productInstanceRepository,
final TermRepository termRepository,
final AccountingService accountingService,
final AccruedInterestRepository accruedInterestRepository,
final DividendDistributionRepository dividendDistributionRepository) {
super();
this.productDefinitionRepository = productDefinitionRepository;
this.productInstanceRepository = productInstanceRepository;
this.termRepository = termRepository;
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 List<ProductInstanceEntity> productInstances =
this.productInstanceRepository.findByProductDefinition(productDefinitionEntity);
productInstances.forEach(productInstanceEntity -> {
if (productInstanceEntity.getState().equals(ACTIVE)) {
final Account account = this.accountingService.findAccount(productInstanceEntity.getAccountIdentifier());
if (account.getBalance() > 0.00D) {
final BigDecimal balance = BigDecimal.valueOf(account.getBalance());
final BigDecimal rate = BigDecimal.valueOf(productDefinitionEntity.getInterest())
.divide(BigDecimal.valueOf(100), INTEREST_PRECISION, BigDecimal.ROUND_HALF_EVEN);
final BigDecimal accruedInterest =
accruedInterest(balance, rate,
this.periodOfInterestPayable(term.getInterestPayable()), accrualDate.lengthOfYear());
if (accruedInterest.compareTo(BigDecimal.ZERO) > 0) {
final Double doubleValue =
accruedInterest.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 BigDecimal rate = BigDecimal.valueOf(dividendDistributionCommand.rate());
final TermEntity term = this.termRepository.findByProductDefinition(productDefinitionEntity);
final List<String> dateRanges = this.dateRanges(dividendDistributionCommand.dueDate(), term.getInterestPayable());
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(BigDecimal.valueOf(account.getBalance()));
} else {
final AccountEntry accountEntry = currentAccountEntries.get(0);
balanceHolder = new BalanceHolder(BigDecimal.valueOf(accountEntry.getBalance()).subtract(BigDecimal.valueOf(accountEntry.getAmount())));
}
final DividendHolder dividendHolder = new DividendHolder();
dateRanges.forEach(dateRange -> {
final List<AccountEntry> accountEntries =
this.accountingService.fetchEntries(account.getIdentifier(), dateRange, Sort.Direction.DESC.name());
if (!accountEntries.isEmpty()) {
balanceHolder.setBalance(BigDecimal.valueOf(accountEntries.get(0).getBalance()));
}
final BigDecimal currentBalance = balanceHolder.getBalance();
dividendHolder.addAmount(
accruedInterest(currentBalance, rate, 12, dividendDistributionCommand.dueDate().lengthOfYear())
);
});
if (dividendHolder.getAmount().compareTo(BigDecimal.ZERO) > 0) {
final String roundedAmount =
dividendHolder.getAmount()
.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 BigDecimal balance;
private BalanceHolder(final BigDecimal balance) {
super();
this.balance = balance;
}
private BigDecimal getBalance() {
return this.balance;
}
private void setBalance(final BigDecimal balance) {
this.balance = balance;
}
}
private class DividendHolder {
private BigDecimal amount;
private DividendHolder() {
super();
this.amount = BigDecimal.ZERO;
}
private void addAmount(final BigDecimal toAdd) {
this.amount = this.amount.add(toAdd);
}
private BigDecimal 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);
}
/**
* Copied from JavaMoney AnnualPercentageYield.calculate, and adjusted.
* @return the resulting amount, never null.
*/
private static BigDecimal accruedInterest(
final BigDecimal amount,
final BigDecimal rate,
final int periods,
final int lengthOfYear) {
final BigDecimal baseFactor = rate.divide(BigDecimal.valueOf(periods),MathContext.DECIMAL64)
.add(BigDecimal.ONE);
final BigDecimal annualInterest = amount.multiply(baseFactor.pow(periods).subtract(BigDecimal.ONE));
return annualInterest
.divide(BigDecimal.valueOf(lengthOfYear),
amount.scale() + INTEREST_PRECISION, BigDecimal.ROUND_HALF_EVEN);
}
}