FINERACT-2312: Accruals added for savings accounts (#4885)
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@ public enum JobName {
|
||||
PURGE_EXTERNAL_EVENTS("Purge External Events"), //
|
||||
PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), //
|
||||
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), //
|
||||
;
|
||||
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"); //
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
+1
@@ -104,6 +104,7 @@ public class SavingsApiConstants {
|
||||
public static final String activeParamName = "active";
|
||||
public static final String nameParamName = "name";
|
||||
public static final String shortNameParamName = "shortName";
|
||||
public static final String interestReceivableAccount = "interestReceivableAccountId";
|
||||
public static final String descriptionParamName = "description";
|
||||
public static final String currencyCodeParamName = "currencyCode";
|
||||
public static final String digitsAfterDecimalParamName = "digitsAfterDecimal";
|
||||
|
||||
+10
@@ -64,6 +64,10 @@ public final class PostingPeriod {
|
||||
|
||||
private Integer financialYearBeginningMonth;
|
||||
|
||||
public void setOverdraftInterestRateAsFraction(BigDecimal overdraftInterestRateAsFraction) {
|
||||
this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction;
|
||||
}
|
||||
|
||||
public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance,
|
||||
final List<SavingsAccountTransactionDetailsForPostingPeriod> orderedListOfTransactions, final MonetaryCurrency currency,
|
||||
final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType,
|
||||
@@ -545,4 +549,10 @@ public final class PostingPeriod {
|
||||
return this.financialYearBeginningMonth;
|
||||
}
|
||||
|
||||
// public List<CompoundingPeriod> getCompoundingPeriods() {return compoundingPeriods;}
|
||||
|
||||
public Money getClosingBalance() {
|
||||
return closingBalance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-3
@@ -28,6 +28,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivi
|
||||
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||
import org.apache.fineract.accounting.journalentry.data.SavingsDTO;
|
||||
import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -182,9 +183,21 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc
|
||||
else if (savingsTransactionDTO.getTransactionType().isAccrual()) {
|
||||
// Post journal entry for Accrual Recognition
|
||||
if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||
AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(),
|
||||
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
|
||||
if (MathUtil.isGreaterThanZero(overdraftAmount)) {
|
||||
this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||
AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), savingsProductId, paymentTypeId, savingsId,
|
||||
transactionId, transactionDate, amount, isReversal);
|
||||
this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId,
|
||||
transactionId, transactionDate, amount, isReversal);
|
||||
} else {
|
||||
this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||
AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), savingsProductId, paymentTypeId, savingsId,
|
||||
transactionId, transactionDate, amount, isReversal);
|
||||
this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||
AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), savingsProductId, paymentTypeId, savingsId,
|
||||
transactionId, transactionDate, amount, isReversal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.portfolio.savings.jobs.addaccrualtransactionforsavings;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class AddAccrualTransactionForSavingsConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private SavingsAccrualWritePlatformService savingsAccrualWritePlatformService;
|
||||
|
||||
@Bean
|
||||
protected Step addAccrualTransactionForSavingsStep() {
|
||||
return new StepBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository)
|
||||
.tasklet(addAccrualTransactionForSavingsTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job addAccrualTransactionForSavingsJob() {
|
||||
return new JobBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository)
|
||||
.start(addAccrualTransactionForSavingsStep()).incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AddAccrualTransactionForSavingsTasklet addAccrualTransactionForSavingsTasklet() {
|
||||
return new AddAccrualTransactionForSavingsTasklet(savingsAccrualWritePlatformService);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.portfolio.savings.jobs.addaccrualtransactionforsavings;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.MultiException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class AddAccrualTransactionForSavingsTasklet implements Tasklet {
|
||||
|
||||
private final SavingsAccrualWritePlatformService savingsAccrualWritePlatformService;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||
try {
|
||||
addPeriodicAccruals(DateUtils.getBusinessLocalDate());
|
||||
} catch (MultiException e) {
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
|
||||
private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException {
|
||||
savingsAccrualWritePlatformService.addAccrualEntries(tilldate);
|
||||
}
|
||||
}
|
||||
+11
@@ -64,7 +64,9 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountSubStatusEnumDat
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
@@ -1386,4 +1388,13 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
public Long retrieveAccountIdByExternalId(final ExternalId externalId) {
|
||||
return savingsAccountRepositoryWrapper.findIdByExternalId(externalId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SavingsAccrualData> retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) {
|
||||
Long savingsId = (savings != null) ? savings.getId() : null;
|
||||
Integer status = SavingsAccountStatusType.ACTIVE.getValue();
|
||||
Integer accountingRule = AccountingRuleType.ACCRUAL_PERIODIC.getValue();
|
||||
|
||||
return this.savingsAccountRepositoryWrapper.findAccrualData(tillDate, savingsId, status, accountingRule);
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.core.exception.MultiException;
|
||||
|
||||
public interface SavingsAccrualWritePlatformService {
|
||||
|
||||
void addAccrualEntries(LocalDate tillDate) throws MultiException;
|
||||
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsHelper;
|
||||
import org.apache.fineract.portfolio.savings.domain.interest.CompoundInterestValues;
|
||||
import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod;
|
||||
import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SavingsAccrualWritePlatformServiceImpl implements SavingsAccrualWritePlatformService {
|
||||
|
||||
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
|
||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
|
||||
private final SavingsHelper savingsHelper;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final SavingsAccountDomainService savingsAccountDomainService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void addAccrualEntries(LocalDate tillDate) throws JobExecutionException {
|
||||
final List<SavingsAccrualData> savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, null);
|
||||
final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
|
||||
List<Throwable> errors = new ArrayList<>();
|
||||
for (SavingsAccrualData savingsAccrual : savingsAccrualData) {
|
||||
try {
|
||||
if (savingsAccrual.getDepositType().isSavingsDeposit() && savingsAccrual.getIsAllowOverdraft()) {
|
||||
if (!savingsAccrual.getIsTypeInterestReceivable()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccrual.getId(), false);
|
||||
LocalDate fromDate = savingsAccrual.getAccruedTill();
|
||||
if (fromDate == null) {
|
||||
fromDate = savingsAccount.getActivationDate();
|
||||
}
|
||||
log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate);
|
||||
addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, mc, null);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage());
|
||||
errors.add(e.getCause());
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw new JobExecutionException(errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAccrualTransactions(SavingsAccount savingsAccount, final LocalDate fromDate, final LocalDate tillDate,
|
||||
final Integer financialYearBeginningMonth, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final MathContext mc,
|
||||
final Function<LocalDate, String> refNoProvider) {
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
existingTransactionIds.addAll(savingsAccount.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(savingsAccount.findExistingReversedTransactionIds());
|
||||
|
||||
List<LocalDate> postedAsOnTransactionDates = savingsAccount.getManualPostingDates();
|
||||
final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType
|
||||
.fromInt(savingsAccount.getInterestCalculationType());
|
||||
|
||||
final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType
|
||||
.fromInt(savingsAccount.getInterestPostingPeriodType());
|
||||
|
||||
final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType
|
||||
.fromInt(savingsAccount.getInterestCalculationDaysInYearType());
|
||||
|
||||
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(fromDate, tillDate,
|
||||
postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates);
|
||||
|
||||
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
|
||||
final MonetaryCurrency currency = savingsAccount.getCurrency();
|
||||
Money periodStartingBalance = Money.zero(currency);
|
||||
|
||||
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType
|
||||
.fromInt(savingsAccount.getInterestCalculationType());
|
||||
final BigDecimal interestRateAsFraction = savingsAccount.getEffectiveInterestRateAsFractionAccrual(mc, tillDate);
|
||||
final Collection<Long> interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(savingsAccount.getId());
|
||||
boolean isInterestTransfer = false;
|
||||
final Money minBalanceForInterestCalculation = Money.of(currency, savingsAccount.getMinBalanceForInterestCalculation());
|
||||
List<SavingsAccountTransactionDetailsForPostingPeriod> savingsAccountTransactionDetailsForPostingPeriodList = savingsAccount
|
||||
.toSavingsAccountTransactionDetailsForPostingPeriodList();
|
||||
for (final LocalDateInterval periodInterval : postingPeriodIntervals) {
|
||||
if (DateUtils.isDateInTheFuture(periodInterval.endDate())) {
|
||||
continue;
|
||||
}
|
||||
final boolean isUserPosting = postedAsOnTransactionDates.contains(periodInterval.endDate());
|
||||
|
||||
final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance,
|
||||
savingsAccountTransactionDetailsForPostingPeriodList, currency, compoundingPeriodType, interestCalculationType,
|
||||
interestRateAsFraction, daysInYearType.getValue(), tillDate, interestPostTransactions, isInterestTransfer,
|
||||
minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting,
|
||||
financialYearBeginningMonth);
|
||||
|
||||
postingPeriod.setOverdraftInterestRateAsFraction(
|
||||
savingsAccount.getNominalAnnualInterestRateOverdraft().divide(BigDecimal.valueOf(100), mc));
|
||||
periodStartingBalance = postingPeriod.closingBalance();
|
||||
|
||||
allPostingPeriods.add(postingPeriod);
|
||||
}
|
||||
BigDecimal compoundedInterest = BigDecimal.ZERO;
|
||||
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
|
||||
final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
|
||||
|
||||
final List<LocalDate> accrualTransactionDates = savingsAccount.retrieveOrderedAccrualTransactions().stream()
|
||||
.map(transaction -> transaction.getTransactionDate()).toList();
|
||||
LocalDate accruedTillDate = fromDate;
|
||||
|
||||
for (PostingPeriod period : allPostingPeriods) {
|
||||
period.calculateInterest(compoundInterestValues);
|
||||
final LocalDate endDate = period.getPeriodInterval().endDate();
|
||||
if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate())
|
||||
&& !MathUtil.isZero(period.closingBalance().getAmount())) {
|
||||
String refNo = (refNoProvider != null) ? refNoProvider.apply(endDate) : null;
|
||||
SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount,
|
||||
savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned().abs(), false, refNo);
|
||||
savingsAccountTransaction.setRunningBalance(period.getClosingBalance());
|
||||
savingsAccountTransaction.setOverdraftAmount(period.getInterestEarned());
|
||||
savingsAccount.addTransaction(savingsAccountTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
savingsAccount.setAccruedTillDate(accruedTillDate);
|
||||
savingsAccountRepository.saveAndFlush(savingsAccount);
|
||||
savingsAccountDomainService.postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<!-- Add new module to the end of this modules list (to keep the existing auto-increment identifiers) -->
|
||||
<include file="db/changelog/tenant/module/loan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<include file="db/changelog/tenant/module/investor/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<include file="db/changelog/tenant/module/savings/parts/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND !initial_switch AND custom_changelog"/>
|
||||
<include file="/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<!-- Scripts to run after the modules were initialized -->
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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.portfolio.savings.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class SavingsAccrualData {
|
||||
|
||||
private final Long id;
|
||||
private final String accountNo;
|
||||
private final LocalDate accruedTill;
|
||||
private final Boolean isTypeInterestReceivable;
|
||||
private final Boolean isAllowOverdraft;
|
||||
private final Integer depositType;
|
||||
|
||||
public DepositAccountType getDepositType() {
|
||||
final EnumOptionData depositType = SavingsEnumerations.depositType(this.depositType);
|
||||
DepositAccountType depositAccountType = DepositAccountType.fromInt(depositType.getId().intValue());
|
||||
return depositAccountType;
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -71,6 +71,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
@@ -337,6 +338,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
@JoinColumn(name = "tax_group_id")
|
||||
private TaxGroup taxGroup;
|
||||
|
||||
@Column(name = "accrued_till_date")
|
||||
private LocalDate accruedTillDate;
|
||||
|
||||
@Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true)
|
||||
private BigDecimal savingsOnHoldAmount;
|
||||
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@@ -926,6 +930,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc);
|
||||
}
|
||||
|
||||
public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) {
|
||||
return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) {
|
||||
return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
||||
@@ -939,6 +947,11 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft);
|
||||
}
|
||||
|
||||
public List<SavingsAccountTransaction> retrieveOrderedAccrualTransactions() {
|
||||
return retrieveListOfTransactions().stream().filter(SavingsAccountTransaction::isAccrual)
|
||||
.sorted(new SavingsAccountTransactionComparator()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected List<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() {
|
||||
final List<SavingsAccountTransaction> listOfTransactionsSorted = retrieveListOfTransactions();
|
||||
|
||||
@@ -3841,10 +3854,21 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return this.withHoldTax;
|
||||
}
|
||||
|
||||
public void setAccruedTillDate(LocalDate accruedTillDate) {
|
||||
this.accruedTillDate = accruedTillDate;
|
||||
}
|
||||
|
||||
public List<SavingsAccountTransactionDetailsForPostingPeriod> toSavingsAccountTransactionDetailsForPostingPeriodList(
|
||||
List<SavingsAccountTransaction> transactions) {
|
||||
return transactions.stream()
|
||||
.map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<SavingsAccountTransactionDetailsForPostingPeriod> toSavingsAccountTransactionDetailsForPostingPeriodList() {
|
||||
return retreiveOrderedNonInterestPostingTransactions().stream()
|
||||
.map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
@@ -19,8 +19,10 @@
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -70,4 +72,26 @@ public interface SavingsAccountRepository extends JpaRepository<SavingsAccount,
|
||||
|
||||
@Query("SELECT sa.id FROM SavingsAccount sa WHERE sa.externalId = :externalId")
|
||||
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
||||
|
||||
@Query("""
|
||||
SELECT new org.apache.fineract.portfolio.savings.data.SavingsAccrualData(
|
||||
savings.id,
|
||||
savings.accountNumber,
|
||||
savings.accruedTillDate,
|
||||
CASE WHEN apm.financialAccountType = 18 THEN TRUE ELSE FALSE END,
|
||||
msp.allowOverdraft,
|
||||
savings.depositType
|
||||
)
|
||||
FROM SavingsAccount savings
|
||||
LEFT JOIN SavingsProduct msp ON msp = savings.product
|
||||
LEFT JOIN ProductToGLAccountMapping apm ON apm.productId = msp.id and (apm.financialAccountType = 18 or apm.financialAccountType IS NULL)
|
||||
WHERE savings.status = :status
|
||||
AND (savings.nominalAnnualInterestRate IS NOT NULL AND savings.nominalAnnualInterestRate > 0)
|
||||
AND msp.accountingRule = :accountingRule
|
||||
AND ( savings.closedOnDate <= :tillDate OR savings.closedOnDate IS NULL)
|
||||
AND ( savings.accruedTillDate <= :tillDate OR savings.accruedTillDate IS NULL )
|
||||
ORDER BY savings.id
|
||||
""")
|
||||
List<SavingsAccrualData> findAccrualData(@Param("tillDate") LocalDate tillDate, @Param("savingsId") Long savingsId,
|
||||
@Param("status") Integer status, @Param("accountingRule") Integer accountingRule);
|
||||
}
|
||||
|
||||
+7
@@ -22,6 +22,7 @@ import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -179,4 +180,10 @@ public class SavingsAccountRepositoryWrapper {
|
||||
public Long findIdByExternalId(final ExternalId externalId) {
|
||||
return this.repository.findIdByExternalId(externalId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SavingsAccrualData> findAccrualData(final LocalDate tillDate, final Long savingsId, final Integer status,
|
||||
final Integer accountingRule) {
|
||||
return this.repository.findAccrualData(tillDate, savingsId, status, accountingRule);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -198,6 +198,14 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
|
||||
date, amount, isReversed, isManualTransaction, lienTransaction, refNo);
|
||||
}
|
||||
|
||||
public static SavingsAccountTransaction accrual(final SavingsAccount savingsAccount, final Office office, final LocalDate date,
|
||||
final Money amount, final boolean isManualTransaction, final String refNo) {
|
||||
final boolean isReversed = false;
|
||||
final Boolean lienTransaction = false;
|
||||
return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount,
|
||||
isReversed, isManualTransaction, lienTransaction, refNo);
|
||||
}
|
||||
|
||||
public static SavingsAccountTransaction interestPosting(final SavingsAccount savingsAccount, final Office office, final LocalDate date,
|
||||
final Money amount, final boolean isManualTransaction) {
|
||||
final boolean isReversed = false;
|
||||
@@ -415,7 +423,7 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
|
||||
return Money.of(currency, this.overdraftAmount);
|
||||
}
|
||||
|
||||
void setOverdraftAmount(Money overdraftAmount) {
|
||||
public void setOverdraftAmount(Money overdraftAmount) {
|
||||
this.overdraftAmount = overdraftAmount == null ? null : overdraftAmount.getAmount();
|
||||
}
|
||||
|
||||
@@ -511,6 +519,10 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
|
||||
return this.isDeposit() || this.isWithdrawal() || this.isChargeTransaction() || this.isDividendPayout() || this.isInterestPosting();
|
||||
}
|
||||
|
||||
public boolean isAccrual() {
|
||||
return getTransactionType().isAccrual();
|
||||
}
|
||||
|
||||
public boolean isInterestPostingAndNotReversed() {
|
||||
return getTransactionType().isInterestPosting() && isNotReversed();
|
||||
}
|
||||
|
||||
+4
@@ -27,6 +27,8 @@ import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
|
||||
public interface SavingsAccountReadPlatformService {
|
||||
|
||||
@@ -69,4 +71,6 @@ public interface SavingsAccountReadPlatformService {
|
||||
List<SavingsAccountTransactionData> retrieveAllTransactionData(List<String> refNo);
|
||||
|
||||
Long retrieveAccountIdByExternalId(ExternalId externalId);
|
||||
|
||||
List<SavingsAccrualData> retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings);
|
||||
}
|
||||
|
||||
+3
@@ -23,4 +23,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<!-- Sequence is starting from 2000 to make it easier to move existing liquibase changesets here -->
|
||||
<include file="parts/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2002_add_savings_accrual_permission.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2003_add_accrued_till_date_to_savings_account.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="job">
|
||||
<column name="name" value="Add Accrual Transactions For Savings"/>
|
||||
<column name="display_name" value="Add Accrual Transactions For Savings"/>
|
||||
<column name="cron_expression" value="0 1 0 1/1 * ? *"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="Add Accrual Transactions For Savings _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="false"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="0"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
<column name="short_name" value="ADD_ATFS"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="accounting"/>
|
||||
<column name="code" value="EXECUTEFORSAVINGS"/>
|
||||
<column name="entity_name" value="PERIODICACCRUALACCOUNTINGFORSAVINGS"/>
|
||||
<column name="action_name" value="EXECUTE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_savings_account">
|
||||
<column name="accrued_till_date" type="DATE" defaultValueComputed="NULL" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
|
||||
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SavingsAccrualAccountingIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SavingsAccrualAccountingIntegrationTest.class);
|
||||
private ResponseSpecification responseSpec;
|
||||
private RequestSpecification requestSpec;
|
||||
private SavingsAccountHelper savingsAccountHelper;
|
||||
private SchedulerJobHelper schedulerJobHelper;
|
||||
private JournalEntryHelper journalEntryHelper;
|
||||
private AccountHelper accountHelper;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
|
||||
this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
|
||||
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
|
||||
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPositiveAccrualPostsCorrectJournalEntries() {
|
||||
// --- ARRANGE ---
|
||||
LOG.info("------------------------- INITIATING POSITIVE ACCRUAL ACCOUNTING TEST -------------------------");
|
||||
final int daysToSubtract = 10;
|
||||
final String amount = "10000";
|
||||
|
||||
final Account savingsReferenceAccount = this.accountHelper.createAssetAccount("Savings Reference");
|
||||
final Account interestOnSavingsAccount = this.accountHelper.createExpenseAccount("Interest on Savings (Expense)");
|
||||
final Account savingsControlAccount = this.accountHelper.createLiabilityAccount("Savings Control");
|
||||
final Account interestPayableAccount = this.accountHelper.createLiabilityAccount("Interest Payable (Liability)");
|
||||
final Account incomeFromFeesAccount = this.accountHelper.createIncomeAccount("Income from Fees");
|
||||
final Account[] accountList = { savingsReferenceAccount, savingsControlAccount, interestOnSavingsAccount, interestPayableAccount,
|
||||
incomeFromFeesAccount };
|
||||
|
||||
final SavingsProductHelper productHelper = new SavingsProductHelper().withNominalAnnualInterestRate(new BigDecimal("10.0"))
|
||||
.withAccountingRuleAsAccrualBased(accountList)
|
||||
.withSavingsReferenceAccountId(savingsReferenceAccount.getAccountID().toString())
|
||||
.withSavingsControlAccountId(savingsControlAccount.getAccountID().toString())
|
||||
.withInterestOnSavingsAccountId(interestOnSavingsAccount.getAccountID().toString())
|
||||
.withInterestPayableAccountId(interestPayableAccount.getAccountID().toString())
|
||||
.withIncomeFromFeeAccountId(incomeFromFeesAccount.getAccountID().toString());
|
||||
|
||||
final Integer savingsProductId = SavingsProductHelper.createSavingsProduct(productHelper.build(), this.requestSpec,
|
||||
this.responseSpec);
|
||||
Assertions.assertNotNull(savingsProductId, "Failed to create savings product.");
|
||||
|
||||
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2020");
|
||||
final LocalDate startDate = LocalDate.now(Utils.getZoneIdOfTenant()).minusDays(daysToSubtract);
|
||||
final String startDateString = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
|
||||
final Integer savingsAccountId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, savingsProductId,
|
||||
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startDateString);
|
||||
this.savingsAccountHelper.approveSavingsOnDate(savingsAccountId, startDateString);
|
||||
this.savingsAccountHelper.activateSavings(savingsAccountId, startDateString);
|
||||
this.savingsAccountHelper.depositToSavingsAccount(savingsAccountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
|
||||
// --- ACT ---
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions For Savings");
|
||||
|
||||
// --- ASSERT ---
|
||||
List<HashMap> accrualTransactions = getAccrualTransactions(savingsAccountId);
|
||||
Assertions.assertFalse(accrualTransactions.isEmpty(), "No accrual transactions were found.");
|
||||
|
||||
Number firstTransactionIdNumber = (Number) accrualTransactions.get(0).get("id");
|
||||
ArrayList<HashMap> journalEntries = journalEntryHelper.getJournalEntriesByTransactionId("S" + firstTransactionIdNumber.intValue());
|
||||
Assertions.assertFalse(journalEntries.isEmpty(), "No journal entries found for positive accrual.");
|
||||
|
||||
boolean debitFound = false;
|
||||
boolean creditFound = false;
|
||||
for (Map<String, Object> entry : journalEntries) {
|
||||
String entryType = (String) ((HashMap) entry.get("entryType")).get("value");
|
||||
Integer accountId = ((Number) entry.get("glAccountId")).intValue();
|
||||
if ("DEBIT".equals(entryType) && accountId.equals(interestOnSavingsAccount.getAccountID())) {
|
||||
debitFound = true;
|
||||
}
|
||||
if ("CREDIT".equals(entryType) && accountId.equals(interestPayableAccount.getAccountID())) {
|
||||
creditFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertTrue(debitFound, "DEBIT to Interest on Savings (Expense) Account not found for positive accrual.");
|
||||
Assertions.assertTrue(creditFound, "CREDIT to Interest Payable (Liability) Account not found for positive accrual.");
|
||||
|
||||
BigDecimal interest = getCalculateAccrualsForDay(productHelper, amount);
|
||||
|
||||
for (HashMap accrual : accrualTransactions) {
|
||||
BigDecimal amountAccrualTransaccion = BigDecimal.valueOf((Double) accrual.get("amount"));
|
||||
Assertions.assertEquals(interest, amountAccrualTransaccion);
|
||||
}
|
||||
LOG.info("VALIDATE AMOUNT AND ACCOUNT");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeAccrualPostsCorrectJournalEntries() {
|
||||
// --- ARRANGE ---
|
||||
LOG.info("------------------------- INITIATING NEGATIVE ACCRUAL (OVERDRAFT) ACCOUNTING TEST -------------------------");
|
||||
final int daysToSubtract = 10;
|
||||
final String amount = "10000";
|
||||
|
||||
final Account savingsReferenceAccount = this.accountHelper.createAssetAccount("Savings Reference");
|
||||
final Account overdraftPortfolioControl = this.accountHelper.createAssetAccount("Overdraft Portfolio");
|
||||
final Account interestReceivableAccount = this.accountHelper.createAssetAccount("Interest Receivable (Asset)");
|
||||
final Account savingsControlAccount = this.accountHelper.createLiabilityAccount("Savings Control");
|
||||
final Account interestPayableAccount = this.accountHelper.createLiabilityAccount("Interest Payable");
|
||||
final Account overdraftInterestIncomeAccount = this.accountHelper.createIncomeAccount("Overdraft Interest Income");
|
||||
final Account expenseAccount = this.accountHelper.createExpenseAccount("Interest on Savings (Expense)");
|
||||
|
||||
final Account[] accountList = { savingsReferenceAccount, savingsControlAccount, expenseAccount, overdraftInterestIncomeAccount };
|
||||
|
||||
final String overdraftLimit = "10000";
|
||||
final String overdraftInterestRate = "21.0";
|
||||
final SavingsProductHelper productHelper = new SavingsProductHelper()
|
||||
.withNominalAnnualInterestRate(new BigDecimal(overdraftInterestRate)).withAccountingRuleAsAccrualBased(accountList)
|
||||
.withOverDraftRate(overdraftLimit, overdraftInterestRate)
|
||||
.withSavingsReferenceAccountId(savingsReferenceAccount.getAccountID().toString())
|
||||
.withSavingsControlAccountId(savingsControlAccount.getAccountID().toString())
|
||||
.withInterestReceivableAccountId(interestReceivableAccount.getAccountID().toString())
|
||||
.withIncomeFromInterestId(overdraftInterestIncomeAccount.getAccountID().toString())
|
||||
.withInterestPayableAccountId(interestPayableAccount.getAccountID().toString())
|
||||
.withInterestOnSavingsAccountId(expenseAccount.getAccountID().toString())
|
||||
.withOverdraftPortfolioControlId(overdraftPortfolioControl.getAccountID().toString());
|
||||
|
||||
final Integer savingsProductId = SavingsProductHelper.createSavingsProduct(productHelper.build(), this.requestSpec,
|
||||
this.responseSpec);
|
||||
Assertions.assertNotNull(savingsProductId, "Savings product with overdraft creation failed.");
|
||||
|
||||
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2020");
|
||||
final LocalDate startDate = LocalDate.now(Utils.getZoneIdOfTenant()).minusDays(daysToSubtract);
|
||||
final String startDateString = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
|
||||
final Integer savingsAccountId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, savingsProductId,
|
||||
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startDateString);
|
||||
this.savingsAccountHelper.approveSavingsOnDate(savingsAccountId, startDateString);
|
||||
this.savingsAccountHelper.activateSavings(savingsAccountId, startDateString);
|
||||
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsAccountId, "10000", startDateString,
|
||||
CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
|
||||
// --- ACT ---
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions For Savings");
|
||||
|
||||
// --- ASSERT ---
|
||||
List<HashMap> accrualTransactions = getAccrualTransactions(savingsAccountId);
|
||||
Assertions.assertFalse(accrualTransactions.isEmpty(), "No accrual transactions were found for overdraft.");
|
||||
|
||||
Number firstTransactionIdNumber = (Number) accrualTransactions.get(0).get("id");
|
||||
ArrayList<HashMap> journalEntries = journalEntryHelper.getJournalEntriesByTransactionId("S" + firstTransactionIdNumber.intValue());
|
||||
Assertions.assertFalse(journalEntries.isEmpty(), "No journal entries found for negative accrual.");
|
||||
|
||||
boolean debitFound = false;
|
||||
boolean creditFound = false;
|
||||
for (Map<String, Object> entry : journalEntries) {
|
||||
String entryType = (String) ((HashMap) entry.get("entryType")).get("value");
|
||||
Integer accountId = ((Number) entry.get("glAccountId")).intValue();
|
||||
if ("DEBIT".equals(entryType) && accountId.equals(interestReceivableAccount.getAccountID())) {
|
||||
debitFound = true;
|
||||
}
|
||||
if ("CREDIT".equals(entryType) && accountId.equals(overdraftInterestIncomeAccount.getAccountID())) {
|
||||
creditFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assertions.assertTrue(debitFound, "DEBIT to Interest Receivable (Asset) Account not found for negative accrual.");
|
||||
Assertions.assertTrue(creditFound, "CREDIT to Overdraft Interest Income Account not found for negative accrual.");
|
||||
|
||||
BigDecimal interest = getCalculateAccrualsForDay(productHelper, amount);
|
||||
|
||||
for (HashMap accrual : accrualTransactions) {
|
||||
BigDecimal amountAccrualTransaccion = BigDecimal.valueOf((Double) accrual.get("amount"));
|
||||
Assertions.assertEquals(interest, amountAccrualTransaccion);
|
||||
}
|
||||
LOG.info("VALIDATE AMOUNT AND ACCOUNT");
|
||||
}
|
||||
|
||||
private List<HashMap> getAccrualTransactions(Integer savingsAccountId) {
|
||||
List<HashMap> allTransactions = savingsAccountHelper.getSavingsTransactions(savingsAccountId);
|
||||
List<HashMap> accrualTransactions = new ArrayList<>();
|
||||
for (HashMap transaction : allTransactions) {
|
||||
Map<String, Object> type = (Map<String, Object>) transaction.get("transactionType");
|
||||
if (type != null && Boolean.TRUE.equals(type.get("accrual"))) {
|
||||
accrualTransactions.add(transaction);
|
||||
}
|
||||
}
|
||||
return accrualTransactions;
|
||||
}
|
||||
|
||||
private BigDecimal getCalculateAccrualsForDay(SavingsProductHelper productHelper, String amount) {
|
||||
BigDecimal interest = BigDecimal.ZERO;
|
||||
BigDecimal interestRateAsFraction = productHelper.getNominalAnnualInterestRate().divide(new BigDecimal(100.00));
|
||||
BigDecimal realBalanceForInterestCalculation = new BigDecimal(amount);
|
||||
|
||||
final BigDecimal multiplicand = BigDecimal.ONE.divide(productHelper.getInterestCalculationDaysInYearType(), MathContext.DECIMAL64);
|
||||
final BigDecimal dailyInterestRate = interestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64);
|
||||
final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(1), MathContext.DECIMAL64);
|
||||
interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64)
|
||||
.setScale(productHelper.getDecimalCurrency(), RoundingMode.HALF_EVEN);
|
||||
|
||||
return interest;
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
|
||||
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
|
||||
import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SavingsAccrualIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SavingsAccrualIntegrationTest.class);
|
||||
private ResponseSpecification responseSpec;
|
||||
private RequestSpecification requestSpec;
|
||||
private SavingsAccountHelper savingsAccountHelper;
|
||||
private SchedulerJobHelper schedulerJobHelper;
|
||||
private JournalEntryHelper journalEntryHelper;
|
||||
private AccountHelper accountHelper;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
|
||||
this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
|
||||
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
|
||||
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccrualsAreGeneratedForTenDayPeriod() {
|
||||
// --- ARRANGE ---
|
||||
|
||||
final Account assetAccount = this.accountHelper.createAssetAccount();
|
||||
final Account liabilityAccount = this.accountHelper.createLiabilityAccount();
|
||||
final Account incomeAccount = this.accountHelper.createIncomeAccount();
|
||||
final Account expenseAccount = this.accountHelper.createExpenseAccount();
|
||||
final String interestRate = "10.0";
|
||||
final int daysToTest = 10;
|
||||
|
||||
final SavingsProductHelper productHelper = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily()
|
||||
.withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance()
|
||||
.withNominalAnnualInterestRate(new BigDecimal(interestRate))
|
||||
.withAccountingRuleAsAccrualBased(new Account[] { assetAccount, liabilityAccount, incomeAccount, expenseAccount });
|
||||
|
||||
final Integer savingsProductId = SavingsProductHelper.createSavingsProduct(productHelper.build(), this.requestSpec,
|
||||
this.responseSpec);
|
||||
Assertions.assertNotNull(savingsProductId, "Error creating savings product.");
|
||||
|
||||
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2020");
|
||||
Assertions.assertNotNull(clientId, "Error creating client.");
|
||||
|
||||
final LocalDate startDate = LocalDate.now(Utils.getZoneIdOfTenant()).minusDays(daysToTest);
|
||||
final String startDateString = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
|
||||
|
||||
final Integer savingsAccountId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, savingsProductId,
|
||||
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startDateString);
|
||||
Assertions.assertNotNull(savingsAccountId, "Error applying for savings account.");
|
||||
|
||||
this.savingsAccountHelper.approveSavingsOnDate(savingsAccountId, startDateString);
|
||||
this.savingsAccountHelper.activateSavings(savingsAccountId, startDateString);
|
||||
|
||||
final HashMap<String, Object> savingsStatus = SavingsStatusChecker.getStatusOfSavings(this.requestSpec, this.responseSpec,
|
||||
savingsAccountId);
|
||||
SavingsStatusChecker.verifySavingsIsActive(savingsStatus);
|
||||
|
||||
this.savingsAccountHelper.depositToSavingsAccount(savingsAccountId, "10000", startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
|
||||
// --- ACT ---
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions For Savings");
|
||||
|
||||
// --- ASSERT ---
|
||||
List<HashMap> allTransactions = savingsAccountHelper.getSavingsTransactions(savingsAccountId);
|
||||
List<HashMap> accrualTransactions = new ArrayList<>();
|
||||
for (HashMap transaction : allTransactions) {
|
||||
Map<String, Object> type = (Map<String, Object>) transaction.get("transactionType");
|
||||
if (type != null && Boolean.TRUE.equals(type.get("accrual"))) {
|
||||
accrualTransactions.add(transaction);
|
||||
}
|
||||
}
|
||||
Assertions.assertFalse(accrualTransactions.isEmpty(), "No accrual transactions were found.");
|
||||
|
||||
long daysBetween = ChronoUnit.DAYS.between(startDate, LocalDate.now(Utils.getZoneIdOfTenant()));
|
||||
long actualNumberOfTransactions = accrualTransactions.size();
|
||||
|
||||
Assertions.assertTrue(actualNumberOfTransactions >= daysBetween && actualNumberOfTransactions <= daysBetween + 1, "For a period of "
|
||||
+ daysBetween + " days, a close number of transactions was expected, but found " + actualNumberOfTransactions);
|
||||
|
||||
BigDecimal principal = new BigDecimal("10000");
|
||||
BigDecimal rate = new BigDecimal(interestRate).divide(new BigDecimal(100));
|
||||
BigDecimal daysInYear = new BigDecimal("365");
|
||||
|
||||
BigDecimal expectedTotalAccrual = principal.multiply(rate).divide(daysInYear, 8, RoundingMode.HALF_EVEN)
|
||||
.multiply(new BigDecimal(actualNumberOfTransactions)).setScale(2, RoundingMode.HALF_EVEN);
|
||||
|
||||
BigDecimal actualTotalAccrual = savingsAccountHelper.getTotalAccrualAmount(savingsAccountId);
|
||||
|
||||
Assertions.assertEquals(0, expectedTotalAccrual.compareTo(actualTotalAccrual),
|
||||
"The total accrual (" + actualTotalAccrual + ") does not match the expected (" + expectedTotalAccrual + ")");
|
||||
}
|
||||
}
|
||||
+26
@@ -50,6 +50,7 @@ import org.apache.fineract.client.util.JSON;
|
||||
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||
import org.apache.fineract.integrationtests.common.FineractClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -1475,4 +1476,29 @@ public class SavingsAccountHelper {
|
||||
return Utils.performServerGet(requestSpec, responseSpec, url, "");
|
||||
}
|
||||
|
||||
public Integer createSavingsProductWithAccrualAccounting(final Account assetAccount, final Account liabilityAccount,
|
||||
final Account incomeAccount, final Account expenseAccount, final String interestRate) {
|
||||
|
||||
SavingsProductHelper productHelper = new SavingsProductHelper();
|
||||
final Account[] accountList = { assetAccount, liabilityAccount, incomeAccount, expenseAccount };
|
||||
|
||||
final String savingsProductJSON = productHelper.withInterestCompoundingPeriodTypeAsDaily().withInterestPostingPeriodTypeAsMonthly()
|
||||
.withInterestCalculationPeriodTypeAsDailyBalance().withAccountingRuleAsAccrualBased(accountList)
|
||||
.withNominalAnnualInterestRate(new BigDecimal(interestRate)).build();
|
||||
|
||||
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAccrualAmount(Integer savingsId) {
|
||||
List<HashMap> transactions = getSavingsTransactions(savingsId);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (HashMap tx : transactions) {
|
||||
Map<String, Object> type = (Map<String, Object>) tx.get("transactionType");
|
||||
if (type != null && Boolean.TRUE.equals(type.get("accrual"))) {
|
||||
total = total.add(new BigDecimal(String.valueOf(tx.get("amount"))));
|
||||
}
|
||||
}
|
||||
return total.setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+61
@@ -101,6 +101,7 @@ public class SavingsProductHelper {
|
||||
private Boolean withgsimID = null;
|
||||
private Integer gsimID = null;
|
||||
private String nominalAnnualInterestRateOverdraft = null;
|
||||
private String interestPayableAccountId;
|
||||
private String interestReceivableAccountId = null;
|
||||
|
||||
// TODO: Rewrite to use fineract-client instead!
|
||||
@@ -163,6 +164,14 @@ public class SavingsProductHelper {
|
||||
map.put("daysToEscheat", this.daysToEscheat);
|
||||
|
||||
}
|
||||
if (this.accountingRule.equals(ACCRUAL_PERIODIC) && this.interestReceivableAccountId != null) {
|
||||
map.put("interestReceivableAccountId", this.interestReceivableAccountId);
|
||||
}
|
||||
if (this.accountingRule.equals(ACCRUAL_PERIODIC)) {
|
||||
if (this.interestReceivableAccountId != null) {
|
||||
map.put("interestReceivableAccountId", this.interestReceivableAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
String savingsProductCreateJson = new Gson().toJson(map);
|
||||
LOG.info("{}", savingsProductCreateJson);
|
||||
@@ -304,6 +313,58 @@ public class SavingsProductHelper {
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withSavingsReferenceAccountId(final String savingsReferenceAccountId) {
|
||||
this.savingsReferenceAccountId = savingsReferenceAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withSavingsControlAccountId(final String savingsControlAccountId) {
|
||||
this.savingsControlAccountId = savingsControlAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withInterestOnSavingsAccountId(final String interestOnSavingsAccountId) {
|
||||
this.interestOnSavingsAccountId = interestOnSavingsAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withIncomeFromFeeAccountId(final String incomeFromFeeAccountId) {
|
||||
this.incomeFromFeeAccountId = incomeFromFeeAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withInterestPayableAccountId(final String interestPayableAccountId) {
|
||||
this.interestPayableAccountId = interestPayableAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withOverdraftPortfolioControlId(final String overdraftPortfolioControlId) {
|
||||
this.overdraftPortfolioControlId = overdraftPortfolioControlId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withInterestReceivableAccountId(final String interestReceivableAccountId) {
|
||||
this.interestReceivableAccountId = interestReceivableAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SavingsProductHelper withIncomeFromInterestId(final String incomeFromInterestId) {
|
||||
this.incomeFromInterestId = incomeFromInterestId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BigDecimal getNominalAnnualInterestRate() {
|
||||
return new BigDecimal(nominalAnnualInterestRate);
|
||||
}
|
||||
|
||||
public BigDecimal getInterestCalculationDaysInYearType() {
|
||||
return new BigDecimal(interestCalculationDaysInYearType);
|
||||
}
|
||||
|
||||
public Integer getDecimalCurrency() {
|
||||
return Integer.parseInt(DIGITS_AFTER_DECIMAL);
|
||||
}
|
||||
|
||||
// TODO: Rewrite to use fineract-client instead!
|
||||
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
|
||||
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
|
||||
|
||||
Reference in New Issue
Block a user