FINERACT-2312: Accruals added for savings accounts (#4885)
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
index f579fa2..3d767a7 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
@@ -58,7 +58,7 @@
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;
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java
index b0ac6ce..605a185 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java
@@ -104,6 +104,7 @@
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";
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
index f0bce2b..a9decdc 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
@@ -64,6 +64,10 @@
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 @@
return this.financialYearBeginningMonth;
}
+ // public List<CompoundingPeriod> getCompoundingPeriods() {return compoundingPeriods;}
+
+ public Money getClosingBalance() {
+ return closingBalance;
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java
index 4aa1b93..e0b7370 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java
@@ -28,6 +28,7 @@
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 @@
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);
+ }
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java
new file mode 100644
index 0000000..b9b61cc
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java
@@ -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);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java
new file mode 100644
index 0000000..5638221
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java
@@ -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);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
index f84962a..a5de903 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
@@ -64,7 +64,9 @@
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 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);
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java
new file mode 100644
index 0000000..ed6f4c4
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java
@@ -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;
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
new file mode 100644
index 0000000..6fc136b
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
@@ -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);
+ }
+
+}
diff --git a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
index f44f384..f5c4e60 100644
--- a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
+++ b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
@@ -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 -->
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java
new file mode 100644
index 0000000..1ff69ef
--- /dev/null
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java
@@ -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;
+ }
+
+}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
index 5db6e32..e2f226e 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
@@ -71,6 +71,7 @@
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 @@
@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 @@
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 @@
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 @@
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();
+ }
+
}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java
index aaece88..5a41d3a 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java
@@ -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 @@
@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);
}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java
index 3a40633..562a454 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java
@@ -22,6 +22,7 @@
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 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);
+ }
}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java
index 96aef54..51d1175 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java
@@ -198,6 +198,14 @@
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 @@
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 @@
return this.isDeposit() || this.isWithdrawal() || this.isChargeTransaction() || this.isDividendPayout() || this.isInterestPosting();
}
+ public boolean isAccrual() {
+ return getTransactionType().isAccrual();
+ }
+
public boolean isInterestPostingAndNotReversed() {
return getTransactionType().isInterestPosting() && isNotReversed();
}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java
index dc36728..cf408c5 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java
@@ -27,6 +27,8 @@
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 @@
List<SavingsAccountTransactionData> retrieveAllTransactionData(List<String> refNo);
Long retrieveAccountIdByExternalId(ExternalId externalId);
+
+ List<SavingsAccrualData> retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings);
}
diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml
index 8746633..e18006b 100644
--- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml
+++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml
@@ -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>
diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2001_add_savings_accrual_job.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2001_add_savings_accrual_job.xml
new file mode 100644
index 0000000..cc18f1f
--- /dev/null
+++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2001_add_savings_accrual_job.xml
@@ -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>
diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2002_add_savings_accrual_permission.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2002_add_savings_accrual_permission.xml
new file mode 100644
index 0000000..57f5157
--- /dev/null
+++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2002_add_savings_accrual_permission.xml
@@ -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>
diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2003_add_accrued_till_date_to_savings_account.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2003_add_accrued_till_date_to_savings_account.xml
new file mode 100644
index 0000000..1bdd4dd
--- /dev/null
+++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2003_add_accrued_till_date_to_savings_account.xml
@@ -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>
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualAccountingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualAccountingIntegrationTest.java
new file mode 100644
index 0000000..abb7491
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualAccountingIntegrationTest.java
@@ -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;
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
new file mode 100644
index 0000000..6dd1d04
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
@@ -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 + ")");
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
index d400958..8434b98 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
@@ -50,6 +50,7 @@
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 @@
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);
+ }
+
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
index 98bd7ea..2aa7725 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
@@ -101,6 +101,7 @@
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 @@
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 @@
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)