This is an automated email from the ASF dual-hosted git repository.

victorromero pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new b349cf6831 FINERACT-2312: Accruals added for savings accounts (#4885)
b349cf6831 is described below

commit b349cf68317c00841a8e88430dc94d0a72850bc8
Author: Christopher Sayhaed Giron Vega 
<[email protected]>
AuthorDate: Wed Aug 20 15:00:09 2025 -0600

    FINERACT-2312: Accruals added for savings accounts (#4885)
---
 .../infrastructure/jobs/service/JobName.java       |   2 +-
 .../portfolio/savings/SavingsApiConstants.java     |   1 +
 .../savings/domain/interest/PostingPeriod.java     |  10 +
 .../AccrualBasedAccountingProcessorForSavings.java |  19 +-
 .../AddAccrualTransactionForSavingsConfig.java     |  60 +++++
 .../AddAccrualTransactionForSavingsTasklet.java    |  50 ++++
 .../SavingsAccountReadPlatformServiceImpl.java     |  11 +
 .../SavingsAccrualWritePlatformService.java        |  28 +++
 .../SavingsAccrualWritePlatformServiceImpl.java    | 182 +++++++++++++++
 .../resources/db/changelog/db.changelog-master.xml |   1 +
 .../portfolio/savings/data/SavingsAccrualData.java |  45 ++++
 .../portfolio/savings/domain/SavingsAccount.java   |  24 ++
 .../savings/domain/SavingsAccountRepository.java   |  24 ++
 .../domain/SavingsAccountRepositoryWrapper.java    |   7 +
 .../savings/domain/SavingsAccountTransaction.java  |  14 +-
 .../service/SavingsAccountReadPlatformService.java |   4 +
 .../savings/parts/module-changelog-master.xml      |   3 +
 .../parts/parts/2001_add_savings_accrual_job.xml   |  46 ++++
 .../2002_add_savings_accrual_permission.xml}       |  14 +-
 ...3_add_accrued_till_date_to_savings_account.xml} |  11 +-
 .../SavingsAccrualAccountingIntegrationTest.java   | 251 +++++++++++++++++++++
 .../SavingsAccrualIntegrationTest.java             | 145 ++++++++++++
 .../common/savings/SavingsAccountHelper.java       |  26 +++
 .../common/savings/SavingsProductHelper.java       |  61 +++++
 24 files changed, 1028 insertions(+), 11 deletions(-)

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 f579fa2769..3d767a7c88 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 @@ 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;
 
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 b0ac6cecde..605a185d8c 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 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";
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 f0bce2b127..a9decdc097 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 @@ 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;
+    }
+
 }
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 4aa1b935bd..e0b737073a 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.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);
+                    }
                 }
             }
 
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 0000000000..b9b61ccea8
--- /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 0000000000..5638221996
--- /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 f84962affc..a5de90375b 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.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);
+    }
 }
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 0000000000..ed6f4c4ae8
--- /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 0000000000..6fc136b119
--- /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 f44f38478e..f5c4e60ef3 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 0000000000..1ff69ef605
--- /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 5db6e32dc1..e2f226eb46 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.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();
+    }
+
 }
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 aaece884cd..5a41d3ae12 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 @@ 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);
 }
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 3a40633569..562a4548ad 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.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);
+    }
 }
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 96aef54284..51d1175f0c 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 @@ 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();
     }
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 dc36728b53..cf408c5347 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.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);
 }
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 8746633220..e18006b609 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 0000000000..cc18f1f78c
--- /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/module-changelog-master.xml
 
b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2002_add_savings_accrual_permission.xml
similarity index 58%
copy from 
fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml
copy to 
fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2002_add_savings_accrual_permission.xml
index 8746633220..57f5157580 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/parts/2002_add_savings_accrual_permission.xml
@@ -20,7 +20,15 @@
 
 -->
 <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.1.xsd";>
-  <!-- Sequence is starting from 2000 to make it easier to move existing 
liquibase changesets here -->
+                   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/module-changelog-master.xml
 
b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2003_add_accrued_till_date_to_savings_account.xml
similarity index 68%
copy from 
fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml
copy to 
fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2003_add_accrued_till_date_to_savings_account.xml
index 8746633220..1bdd4dd267 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/parts/2003_add_accrued_till_date_to_savings_account.xml
@@ -20,7 +20,12 @@
 
 -->
 <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.1.xsd";>
-  <!-- Sequence is starting from 2000 to make it easier to move existing 
liquibase changesets here -->
+                   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 0000000000..abb74913c5
--- /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 0000000000..6dd1d04ab0
--- /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 d400958621..8434b98d23 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.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);
+    }
+
 }
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 98bd7ea0df..2aa7725cc0 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 @@ 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)

Reply via email to