galovics commented on a change in pull request #2025:
URL: https://github.com/apache/fineract/pull/2025#discussion_r799699402



##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
##########
@@ -1003,7 +1003,7 @@ protected void recalculateDailyBalances(final Money 
openingAccountBalance, final
                     if (overdraftAmount.isGreaterThanZero()) {
                         
accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount());
                     }
-                    accountTransaction.updateRunningBalance(runningBalance);
+                    // accountTransaction.updateRunningBalance(runningBalance);

Review comment:
       I assume this comment was just left in as a mistake. Let's fix it.

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
##########
@@ -97,6 +103,9 @@
     private final SavingsAccountTransactionRepository 
savingsAccountTransactionRepository;
     private final AccountTransfersReadPlatformService 
accountTransfersReadPlatformService;
     private final ChargeRepositoryWrapper chargeRepositoryWrapper;
+    private final JdbcTemplate jdbcTemplate;
+    private final DataSource dataSource;
+    private static final Logger LOG = 
LoggerFactory.getLogger(AccountingProcessorHelper.class);

Review comment:
       Hi @BLasan.
   
   Are these 3 attributes used anywhere in the code? Right now I don't see any 
usage. If they aren't, do you think it's worth removing them?
   Thanks!

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
##########
@@ -230,6 +246,495 @@ public SavingsAccountData retrieveOne(final Long 
accountId) {
         }
     }
 
+    @Override
+    public List<SavingsAccountTransactionData> 
retrieveAllTransactionData(final List<String> refNo) throws DataAccessException 
{
+        String inSql = String.join(",", Collections.nCopies(refNo.size(), 
"?"));
+        String sql = "select " + 
this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in 
(%s)";
+        Object[] params = new Object[refNo.size()];
+        int i = 0;
+        for (String element : refNo) {
+            params[i] = element;
+            i++;
+        }
+        return this.jdbcTemplate.query(String.format(sql, inSql), 
this.savingsAccountTransactionsForBatchMapper, params);
+    }
+
+    @Override
+    public List<SavingsAccountData> 
retrieveAllSavingsDataForInterestPosting(final boolean 
backdatedTxnsAllowedTill, final int pageSize,
+            final Integer status, final Long maxSavingsId) {
+        LocalDate currentDate = DateUtils.getLocalDateOfTenant().minusDays(1);
+
+        String sql = "select " + 
this.savingAccountMapperForInterestPosting.schema()
+                + "join (select a.id from m_savings_account a where a.id > ? 
and a.status_enum = ? limit ?) b on b.id = sa.id ";
+        if (backdatedTxnsAllowedTill) {
+            sql = sql
+                    + "where if (sa.interest_posted_till_date is not null, 
tr.transaction_date >= sa.interest_posted_till_date, tr.transaction_date >= 
sa.activatedon_date) ";
+        }
+
+        sql = sql + "and apm.product_type=2 and sa.interest_posted_till_date<" 
+ java.sql.Date.valueOf(currentDate);
+        sql = sql + " order by sa.id, tr.transaction_date, tr.created_date, 
tr.id";

Review comment:
       Any chance we could use JPQL for this query instead of native SQL? 
Probably we could gain some portability here in the future with JPQL. Right now 
I don't see any native feature in the query hence the question.

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
##########
@@ -230,6 +246,495 @@ public SavingsAccountData retrieveOne(final Long 
accountId) {
         }
     }
 
+    @Override
+    public List<SavingsAccountTransactionData> 
retrieveAllTransactionData(final List<String> refNo) throws DataAccessException 
{
+        String inSql = String.join(",", Collections.nCopies(refNo.size(), 
"?"));
+        String sql = "select " + 
this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in 
(%s)";
+        Object[] params = new Object[refNo.size()];
+        int i = 0;
+        for (String element : refNo) {
+            params[i] = element;
+            i++;
+        }
+        return this.jdbcTemplate.query(String.format(sql, inSql), 
this.savingsAccountTransactionsForBatchMapper, params);
+    }
+
+    @Override
+    public List<SavingsAccountData> 
retrieveAllSavingsDataForInterestPosting(final boolean 
backdatedTxnsAllowedTill, final int pageSize,
+            final Integer status, final Long maxSavingsId) {
+        LocalDate currentDate = DateUtils.getLocalDateOfTenant().minusDays(1);
+
+        String sql = "select " + 
this.savingAccountMapperForInterestPosting.schema()
+                + "join (select a.id from m_savings_account a where a.id > ? 
and a.status_enum = ? limit ?) b on b.id = sa.id ";
+        if (backdatedTxnsAllowedTill) {
+            sql = sql
+                    + "where if (sa.interest_posted_till_date is not null, 
tr.transaction_date >= sa.interest_posted_till_date, tr.transaction_date >= 
sa.activatedon_date) ";
+        }
+
+        sql = sql + "and apm.product_type=2 and sa.interest_posted_till_date<" 
+ java.sql.Date.valueOf(currentDate);
+        sql = sql + " order by sa.id, tr.transaction_date, tr.created_date, 
tr.id";
+
+        List<SavingsAccountData> savingsAccountDataList = 
this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting,
+                new Object[] { maxSavingsId, status, pageSize });
+        for (SavingsAccountData savingsAccountData : savingsAccountDataList) {
+            this.savingAccountAssembler.assembleSavings(savingsAccountData);
+        }
+        return savingsAccountDataList;
+    }
+
+    private static final class SavingAccountMapperForInterestPosting 
implements ResultSetExtractor<List<SavingsAccountData>> {
+
+        private final String schemaSql;
+
+        SavingAccountMapperForInterestPosting() {
+            final StringBuilder sqlBuilder = new StringBuilder(400);
+            sqlBuilder.append("sa.id as id, sa.account_no as accountNo, 
sa.external_id as externalId, ");
+            sqlBuilder.append("sa.deposit_type_enum as depositType, ");
+            sqlBuilder.append("c.id as clientId, c.office_id as 
clientOfficeId, ");
+            sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, 
");
+            sqlBuilder.append("sa.status_enum as statusEnum, ");
+            sqlBuilder.append("sa.sub_status_enum as subStatusEnum, ");
+            sqlBuilder.append("sa.submittedon_date as submittedOnDate,");
+
+            sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,");
+
+            sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,");
+
+            sqlBuilder.append("sa.approvedon_date as approvedOnDate,");
+
+            sqlBuilder.append("sa.activatedon_date as activatedOnDate,");
+
+            sqlBuilder.append("sa.closedon_date as closedOnDate,");
+
+            sqlBuilder.append(
+                    "sa.currency_code as currencyCode, sa.currency_digits as 
currencyDigits, sa.currency_multiplesof as inMultiplesOf, ");
+
+            sqlBuilder.append("sa.nominal_annual_interest_rate as 
nominalAnnualInterestRate, ");
+            sqlBuilder.append("sa.interest_compounding_period_enum as 
interestCompoundingPeriodType, ");
+            sqlBuilder.append("sa.interest_posting_period_enum as 
interestPostingPeriodType, ");
+            sqlBuilder.append("sa.interest_calculation_type_enum as 
interestCalculationType, ");
+            sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum 
as interestCalculationDaysInYearType, ");
+            sqlBuilder.append("sa.min_required_opening_balance as 
minRequiredOpeningBalance, ");
+            sqlBuilder.append("sa.lockin_period_frequency as 
lockinPeriodFrequency,");
+            sqlBuilder.append("sa.lockin_period_frequency_enum as 
lockinPeriodFrequencyType, ");
+            sqlBuilder.append("sa.withdrawal_fee_for_transfer as 
withdrawalFeeForTransfers, ");
+            sqlBuilder.append("sa.allow_overdraft as allowOverdraft, ");
+            sqlBuilder.append("sa.overdraft_limit as overdraftLimit, ");
+            sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as 
nominalAnnualInterestRateOverdraft, ");
+            sqlBuilder.append("sa.min_overdraft_for_interest_calculation as 
minOverdraftForInterestCalculation, ");
+            sqlBuilder.append("sa.total_deposits_derived as totalDeposits, ");
+            sqlBuilder.append("sa.total_withdrawals_derived as 
totalWithdrawals, ");
+            sqlBuilder.append("sa.total_withdrawal_fees_derived as 
totalWithdrawalFees, ");
+            sqlBuilder.append("sa.total_annual_fees_derived as 
totalAnnualFees, ");
+            sqlBuilder.append("sa.total_interest_earned_derived as 
totalInterestEarned, ");
+            sqlBuilder.append("sa.total_interest_posted_derived as 
totalInterestPosted, ");
+            sqlBuilder.append("sa.total_overdraft_interest_derived as 
totalOverdraftInterestDerived, ");
+            sqlBuilder.append("sa.account_balance_derived as accountBalance, 
");
+            sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, 
");
+            sqlBuilder.append("sa.total_penalty_charge_derived as 
totalPenaltyCharge, ");
+            sqlBuilder.append("sa.min_balance_for_interest_calculation as 
minBalanceForInterestCalculation,");
+            sqlBuilder.append("sa.min_required_balance as minRequiredBalance, 
");
+            sqlBuilder.append("sa.enforce_min_required_balance as 
enforceMinRequiredBalance, ");
+            sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, ");
+            sqlBuilder.append("sa.withhold_tax as withHoldTax, ");
+            sqlBuilder.append("sa.total_withhold_tax_derived as 
totalWithholdTax, ");
+            sqlBuilder.append("sa.last_interest_calculation_date as 
lastInterestCalculationDate, ");
+            sqlBuilder.append("sa.total_savings_amount_on_hold as 
onHoldAmount, ");
+            sqlBuilder.append("sa.interest_posted_till_date as 
interestPostedTillDate, ");
+            sqlBuilder.append("tg.id as taxGroupId, ");
+            sqlBuilder.append("(select 
IFNULL(max(sat.transaction_date),sa.activatedon_date) ");
+            sqlBuilder.append("from m_savings_account_transaction as sat ");
+            sqlBuilder.append("where sat.is_reversed = 0 ");
+            sqlBuilder.append("and sat.transaction_type_enum in (1,2) ");
+            sqlBuilder.append("and sat.savings_account_id = sa.id) as 
lastActiveTransactionDate, ");
+            sqlBuilder.append("sp.id as productId, ");
+            sqlBuilder.append("sp.is_dormancy_tracking_active as 
isDormancyTrackingActive, ");
+            sqlBuilder.append("sp.days_to_inactive as daysToInactive, ");
+            sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, ");
+            sqlBuilder.append("sp.days_to_escheat as daysToEscheat, ");
+            sqlBuilder.append("sp.accounting_type as accountingType, ");
+            sqlBuilder.append("tr.id as transactionId, 
tr.transaction_type_enum as transactionType, ");
+            sqlBuilder.append("tr.transaction_date as transactionDate, 
tr.amount as transactionAmount,");
+            sqlBuilder.append("tr.created_date as 
createdDate,tr.cumulative_balance_derived as cumulativeBalance,");
+            sqlBuilder.append("tr.running_balance_derived as runningBalance, 
tr.is_reversed as reversed,");
+            sqlBuilder.append("tr.balance_end_date_derived as balanceEndDate, 
tr.overdraft_amount_derived as overdraftAmount,");
+            sqlBuilder.append("tr.is_manual as manualTransaction,tr.office_id 
as officeId, ");
+            sqlBuilder.append("pd.payment_type_id as 
paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, 
");
+            sqlBuilder.append("pd.receipt_number as receiptNumber, 
pd.bank_number as bankNumber,pd.routing_code as routingCode, ");
+            sqlBuilder.append("pt.value as paymentTypeName, ");
+            sqlBuilder.append("msacpb.amount as paidByAmount, msacpb.id as 
chargesPaidById, ");
+            sqlBuilder.append(
+                    "msac.id as chargeId, msac.amount as chargeAmount, 
msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, ");
+            sqlBuilder.append("txd.id as taxDetailsId, txd.amount as 
taxAmount, ");
+            sqlBuilder.append("apm.gl_account_id as 
glAccountIdForInterestOnSavings, apm1.gl_account_id as 
glAccountIdForSavingsControl, ");
+            sqlBuilder.append(
+                    "mtc.id as taxComponentId, mtc.debit_account_id as 
debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as 
taxPercentage ");
+            sqlBuilder.append("from m_savings_account sa ");
+            sqlBuilder.append("join m_savings_product sp ON sa.product_id = 
sp.id ");
+            sqlBuilder.append("join m_currency curr on curr.code = 
sa.currency_code ");
+            sqlBuilder.append("join m_savings_account_transaction tr on sa.id 
= tr.savings_account_id ");
+            sqlBuilder.append("left join m_payment_detail pd on pd.id = 
tr.payment_detail_id ");
+            sqlBuilder.append("left join m_payment_type pt on pt.id = 
pd.payment_type_id ");
+            sqlBuilder.append("left join m_savings_account_charge_paid_by 
msacpb on msacpb.savings_account_transaction_id = tr.id ");
+            sqlBuilder.append("left join m_savings_account_charge msac on 
msac.id = msacpb.savings_account_charge_id ");
+            sqlBuilder.append("left join m_client c ON c.id = sa.client_id ");
+            sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
+            sqlBuilder.append("left join m_tax_group tg on tg.id = 
sa.tax_group_id ");
+            sqlBuilder.append("left join 
m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = 
tr.id ");
+            sqlBuilder.append("left join m_tax_component mtc on mtc.id = 
txd.tax_component_id ");
+            sqlBuilder.append("left join acc_product_mapping apm on 
apm.product_id = sp.id and apm.financial_account_type=3 ");
+            sqlBuilder.append("left join acc_product_mapping apm1 on 
apm1.product_id = sp.id and apm1.financial_account_type=2 ");
+
+            this.schemaSql = sqlBuilder.toString();
+        }
+
+        public String schema() {
+            return this.schemaSql;
+        }
+
+        @Override
+        public List<SavingsAccountData> extractData(final ResultSet rs) throws 
SQLException {
+
+            List<SavingsAccountData> savingsAccountDataList = new 
ArrayList<>();
+            HashMap<String, Long> savingsMap = new HashMap<>();
+            String currencyCode = null;
+            Integer currencyDigits = null;
+            Integer inMultiplesOf = null;
+            CurrencyData currency = null;
+            HashMap<String, Long> transMap = new HashMap<>();
+            HashMap<String, Long> taxDetails = new HashMap<>();
+            HashMap<String, Long> chargeDetails = new HashMap<>();
+            SavingsAccountTransactionData savingsAccountTransactionData = null;
+            SavingsAccountData savingsAccountData = null;
+            int count = 0;
+
+            while (rs.next()) {
+                final Long id = rs.getLong("id");
+                final Long transactionId = rs.getLong("transactionId");
+                final Long taxDetailId = 
JdbcSupport.getLongDefaultToNullIfZero(rs, "taxDetailsId");
+                final Long taxComponentId = 
JdbcSupport.getLongDefaultToNullIfZero(rs, "taxComponentId");
+                final String accountNo = rs.getString("accountNo");
+                final Long chargeId = rs.getLong("chargeId");
+
+                if (!savingsMap.containsValue(id)) {
+                    if (count > 0) {
+                        savingsAccountDataList.add(savingsAccountData);
+                    }
+                    count++;
+                    savingsMap.put("id", id);
+
+                    final String externalId = rs.getString("externalId");
+                    final Integer depositTypeId = rs.getInt("depositType");
+                    final EnumOptionData depositType = 
SavingsEnumerations.depositType(depositTypeId);
+                    final Long groupId = JdbcSupport.getLong(rs, "groupId");
+                    final Long groupOfficeId = JdbcSupport.getLong(rs, 
"groupOfficeId");
+                    final GroupGeneralData groupGeneralData = new 
GroupGeneralData(groupId, groupOfficeId);
+
+                    final Long clientId = JdbcSupport.getLong(rs, "clientId");
+                    final Long clientOfficeId = JdbcSupport.getLong(rs, 
"clientOfficeId");
+                    final ClientData clientData = 
ClientData.createClientForInterestPosting(clientId, clientOfficeId);
+
+                    final Long glAccountIdForInterestOnSavings = 
rs.getLong("glAccountIdForInterestOnSavings");
+                    final Long glAccountIdForSavingsControl = 
rs.getLong("glAccountIdForSavingsControl");
+
+                    final Long productId = rs.getLong("productId");
+                    final Integer accountType = rs.getInt("accountingType");
+                    final AccountingRuleType accountingRuleType = 
AccountingRuleType.fromInt(accountType);
+                    final EnumOptionData enumOptionDataForAccounting = new 
EnumOptionData(accountType.longValue(),
+                            accountingRuleType.getCode(), 
accountingRuleType.getValue().toString());
+                    final SavingsProductData savingsProductData = 
SavingsProductData.createForInterestPosting(productId,
+                            enumOptionDataForAccounting);
+
+                    final Integer statusEnum = JdbcSupport.getInteger(rs, 
"statusEnum");
+                    final SavingsAccountStatusEnumData status = 
SavingsEnumerations.status(statusEnum);
+                    final Integer subStatusEnum = JdbcSupport.getInteger(rs, 
"subStatusEnum");
+                    final SavingsAccountSubStatusEnumData subStatus = 
SavingsEnumerations.subStatus(subStatusEnum);
+                    final LocalDate lastActiveTransactionDate = 
JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate");
+                    final boolean isDormancyTrackingActive = 
rs.getBoolean("isDormancyTrackingActive");
+                    final Integer numDaysToInactive = 
JdbcSupport.getInteger(rs, "daysToInactive");
+                    final Integer numDaysToDormancy = 
JdbcSupport.getInteger(rs, "daysToDormancy");
+                    final Integer numDaysToEscheat = 
JdbcSupport.getInteger(rs, "daysToEscheat");
+                    Integer daysToInactive = null;
+                    Integer daysToDormancy = null;
+                    Integer daysToEscheat = null;
+
+                    LocalDate localTenantDate = 
DateUtils.getLocalDateOfTenant();
+                    if (isDormancyTrackingActive && 
statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) {
+                        if (subStatusEnum < 
SavingsAccountSubStatusEnum.ESCHEAT.getValue()) {
+                            daysToEscheat = Math.toIntExact(
+                                    ChronoUnit.DAYS.between(localTenantDate, 
lastActiveTransactionDate.plusDays(numDaysToEscheat)));
+                        }
+                        if (subStatusEnum < 
SavingsAccountSubStatusEnum.DORMANT.getValue()) {
+                            daysToDormancy = Math.toIntExact(
+                                    ChronoUnit.DAYS.between(localTenantDate, 
lastActiveTransactionDate.plusDays(numDaysToDormancy)));
+                        }
+                        if (subStatusEnum < 
SavingsAccountSubStatusEnum.INACTIVE.getValue()) {
+                            daysToInactive = Math.toIntExact(
+                                    ChronoUnit.DAYS.between(localTenantDate, 
lastActiveTransactionDate.plusDays(numDaysToInactive)));
+                        }
+                    }
+                    final LocalDate approvedOnDate = 
JdbcSupport.getLocalDate(rs, "approvedOnDate");
+                    final LocalDate withdrawnOnDate = 
JdbcSupport.getLocalDate(rs, "withdrawnOnDate");
+                    final LocalDate submittedOnDate = 
JdbcSupport.getLocalDate(rs, "submittedOnDate");
+                    final LocalDate activatedOnDate = 
JdbcSupport.getLocalDate(rs, "activatedOnDate");
+                    final LocalDate closedOnDate = 
JdbcSupport.getLocalDate(rs, "closedOnDate");
+                    final SavingsAccountApplicationTimelineData timeline = new 
SavingsAccountApplicationTimelineData(submittedOnDate, null,
+                            null, null, null, null, null, null, 
withdrawnOnDate, null, null, null, approvedOnDate, null, null, null,
+                            activatedOnDate, null, null, null, closedOnDate, 
null, null, null);
+
+                    currencyCode = rs.getString("currencyCode");
+                    currencyDigits = JdbcSupport.getInteger(rs, 
"currencyDigits");
+                    inMultiplesOf = JdbcSupport.getInteger(rs, 
"inMultiplesOf");
+                    currency = new CurrencyData(currencyCode, currencyDigits, 
inMultiplesOf);
+
+                    final BigDecimal totalDeposits = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits");
+                    final BigDecimal totalWithdrawals = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals");
+                    final BigDecimal totalWithdrawalFees = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees");
+                    final BigDecimal totalAnnualFees = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees");
+
+                    final BigDecimal totalInterestEarned = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned");
+                    final BigDecimal totalInterestPosted = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted");
+                    final BigDecimal accountBalance = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance");
+                    final BigDecimal totalFeeCharge = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge");
+                    final BigDecimal totalPenaltyCharge = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge");
+                    final BigDecimal totalOverdraftInterestDerived = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
+                            "totalOverdraftInterestDerived");
+                    final BigDecimal totalWithholdTax = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax");
+                    final LocalDate interestPostedTillDate = 
JdbcSupport.getLocalDate(rs, "interestPostedTillDate");
+
+                    final BigDecimal minBalanceForInterestCalculation = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs,
+                            "minBalanceForInterestCalculation");
+                    final BigDecimal onHoldFunds = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldFunds");
+
+                    final BigDecimal onHoldAmount = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldAmount");
+
+                    BigDecimal availableBalance = accountBalance;
+                    if (availableBalance != null && onHoldFunds != null) {
+
+                        availableBalance = 
availableBalance.subtract(onHoldFunds);
+                    }
+
+                    if (availableBalance != null && onHoldAmount != null) {
+
+                        availableBalance = 
availableBalance.subtract(onHoldAmount);
+                    }
+
+                    BigDecimal interestNotPosted = BigDecimal.ZERO;
+                    LocalDate lastInterestCalculationDate = null;
+                    if (totalInterestEarned != null) {
+                        interestNotPosted = 
totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived);
+                        lastInterestCalculationDate = 
JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate");
+                    }
+
+                    final SavingsAccountSummaryData summary = new 
SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals,
+                            totalWithdrawalFees, totalAnnualFees, 
totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge,
+                            totalPenaltyCharge, totalOverdraftInterestDerived, 
totalWithholdTax, interestNotPosted,
+                            lastInterestCalculationDate, availableBalance, 
interestPostedTillDate);
+                    
summary.setPrevInterestPostedTillDate(interestPostedTillDate);
+
+                    final boolean withHoldTax = rs.getBoolean("withHoldTax");
+                    final Long taxGroupId = 
JdbcSupport.getLongDefaultToNullIfZero(rs, "taxGroupId");
+                    TaxGroupData taxGroupData = null;
+                    if (taxGroupId != null) {
+                        taxGroupData = TaxGroupData.lookup(taxGroupId, null);
+                    }
+
+                    final BigDecimal nominalAnnualInterestRate = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs,
+                            "nominalAnnualInterestRate");
+
+                    final EnumOptionData interestCompoundingPeriodType = 
SavingsEnumerations.compoundingInterestPeriodType(
+                            
SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, 
"interestCompoundingPeriodType")));
+
+                    final EnumOptionData interestPostingPeriodType = 
SavingsEnumerations.interestPostingPeriodType(
+                            
SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, 
"interestPostingPeriodType")));
+
+                    final EnumOptionData interestCalculationType = 
SavingsEnumerations.interestCalculationType(
+                            
SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, 
"interestCalculationType")));
+
+                    final EnumOptionData interestCalculationDaysInYearType = 
SavingsEnumerations
+                            
.interestCalculationDaysInYearType(SavingsInterestCalculationDaysInYearType
+                                    .fromInt(JdbcSupport.getInteger(rs, 
"interestCalculationDaysInYearType")));
+
+                    final BigDecimal minRequiredOpeningBalance = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs,
+                            "minRequiredOpeningBalance");
+
+                    final Integer lockinPeriodFrequency = 
JdbcSupport.getInteger(rs, "lockinPeriodFrequency");
+                    EnumOptionData lockinPeriodFrequencyType = null;
+                    final Integer lockinPeriodFrequencyTypeValue = 
JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType");
+                    if (lockinPeriodFrequencyTypeValue != null) {
+                        final SavingsPeriodFrequencyType lockinPeriodType = 
SavingsPeriodFrequencyType
+                                .fromInt(lockinPeriodFrequencyTypeValue);
+                        lockinPeriodFrequencyType = 
SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType);
+                    }
+
+                    final boolean withdrawalFeeForTransfers = 
rs.getBoolean("withdrawalFeeForTransfers");
+
+                    final boolean allowOverdraft = 
rs.getBoolean("allowOverdraft");
+                    final BigDecimal overdraftLimit = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit");
+                    final BigDecimal nominalAnnualInterestRateOverdraft = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs,
+                            "nominalAnnualInterestRateOverdraft");
+                    final BigDecimal minOverdraftForInterestCalculation = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs,
+                            "minOverdraftForInterestCalculation");
+
+                    final BigDecimal minRequiredBalance = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance");
+                    final boolean enforceMinRequiredBalance = 
rs.getBoolean("enforceMinRequiredBalance");
+                    savingsAccountData = SavingsAccountData.instance(id, 
accountNo, depositType, externalId, null, null, null, null,
+                            productId, null, null, null, status, subStatus, 
timeline, currency, nominalAnnualInterestRate,
+                            interestCompoundingPeriodType, 
interestPostingPeriodType, interestCalculationType,
+                            interestCalculationDaysInYearType, 
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType,
+                            withdrawalFeeForTransfers, summary, 
allowOverdraft, overdraftLimit, minRequiredBalance,
+                            enforceMinRequiredBalance, 
minBalanceForInterestCalculation, onHoldFunds, 
nominalAnnualInterestRateOverdraft,
+                            minOverdraftForInterestCalculation, withHoldTax, 
taxGroupData, lastActiveTransactionDate,
+                            isDormancyTrackingActive, daysToInactive, 
daysToDormancy, daysToEscheat, onHoldAmount);
+
+                    savingsAccountData.setClientData(clientData);
+                    savingsAccountData.setGroupGeneralData(groupGeneralData);
+                    savingsAccountData.setSavingsProduct(savingsProductData);
+                    
savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings);
+                    
savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl);
+                }
+
+                if (!transMap.containsValue(transactionId)) {
+
+                    final int transactionTypeInt = JdbcSupport.getInteger(rs, 
"transactionType");
+                    final SavingsAccountTransactionEnumData transactionType = 
SavingsEnumerations.transactionType(transactionTypeInt);
+
+                    final LocalDate date = JdbcSupport.getLocalDate(rs, 
"transactionDate");
+                    final LocalDate balanceEndDate = 
JdbcSupport.getLocalDate(rs, "balanceEndDate");
+                    final LocalDate transSubmittedOnDate = 
JdbcSupport.getLocalDate(rs, "createdDate");
+                    final BigDecimal amount = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount");
+                    final BigDecimal overdraftAmount = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overdraftAmount");
+                    final BigDecimal outstandingChargeAmount = null;
+                    final BigDecimal runningBalance = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance");
+                    final boolean reversed = rs.getBoolean("reversed");
+                    final boolean isManualTransaction = 
rs.getBoolean("manualTransaction");
+                    final Long officeId = rs.getLong("officeId");
+                    final BigDecimal cumulativeBalance = 
JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "cumulativeBalance");
+
+                    final boolean postInterestAsOn = false;
+
+                    PaymentDetailData paymentDetailData = null;
+                    if (transactionType.isDepositOrWithdrawal()) {
+                        final Long paymentTypeId = JdbcSupport.getLong(rs, 
"paymentType");
+                        if (paymentTypeId != null) {
+                            final String typeName = 
rs.getString("paymentTypeName");
+                            final PaymentTypeData paymentTypeData = new 
PaymentTypeData(paymentTypeId, typeName, null, false, null);
+                            paymentDetailData = new PaymentDetailData(id, 
paymentTypeData, null, null, null, null, null);
+                        }
+                    }
+
+                    savingsAccountTransactionData = 
SavingsAccountTransactionData.create(transactionId, transactionType, 
paymentDetailData,
+                            id, accountNo, date, currency, amount, 
outstandingChargeAmount, runningBalance, reversed, transSubmittedOnDate,
+                            postInterestAsOn, cumulativeBalance, 
balanceEndDate);
+                    
savingsAccountTransactionData.setOverdraftAmount(overdraftAmount);
+
+                    transMap.put("id", transactionId);
+                    if (savingsAccountData.getOfficeId() == null) {
+                        savingsAccountData.setOfficeId(officeId);
+                    }
+
+                    
savingsAccountData.setSavingsAccountTransactionData(savingsAccountTransactionData);
+                }
+
+                if (chargeId != null && 
!chargeDetails.containsValue(chargeId)) {
+                    final boolean isPenalty = rs.getBoolean("isPenaltyCharge");
+                    final BigDecimal chargeAmount = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "chargeAmount");
+                    final Integer chargesTimeType = 
rs.getInt("chargeTimeType");
+                    final EnumOptionData enumOptionDataForChargesTimeType = 
new EnumOptionData(chargesTimeType.longValue(), null, null);
+                    final SavingsAccountChargeData savingsAccountChargeData = 
new SavingsAccountChargeData(chargeId, chargeAmount,
+                            enumOptionDataForChargesTimeType, isPenalty);
+
+                    final Long chargesPaidById = rs.getLong("chargesPaidById");
+                    final BigDecimal chargesPaid = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "paidByAmount");
+                    final SavingsAccountChargesPaidByData 
savingsAccountChargesPaidByData = new SavingsAccountChargesPaidByData(
+                            chargesPaidById, chargesPaid);
+                    
savingsAccountChargesPaidByData.setSavingsAccountChargeData(savingsAccountChargeData);
+                    if (savingsAccountChargesPaidByData != null) {
+                        
savingsAccountTransactionData.setChargesPaidByData(savingsAccountChargesPaidByData);
+                    }
+
+                    chargeDetails.put("id", chargeId);
+                }
+
+                if (taxDetailId != null && 
!taxDetails.containsValue(taxDetailId)) {
+                    final BigDecimal amount = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxAmount");
+                    final BigDecimal percentage = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxPercentage");
+                    final Long debitId = rs.getLong("debitAccountId");
+                    final Long creditId = rs.getLong("creditAccountId");
+                    final GLAccountData debitAccount = 
GLAccountData.createFrom(debitId);
+                    final GLAccountData creditAccount = 
GLAccountData.createFrom(creditId);
+
+                    if (taxComponentId != null) {
+                        final TaxComponentData taxComponent = 
TaxComponentData.createTaxComponent(taxComponentId, percentage, debitAccount,
+                                creditAccount);
+                        savingsAccountTransactionData.setTaxDetails(new 
TaxDetailsData(taxComponent, amount));
+                    }
+
+                    taxDetails.put("id", taxDetailId);
+                }
+
+            }
+            if (savingsAccountData != null) {
+                savingsAccountDataList.add(savingsAccountData);
+            }
+            return savingsAccountDataList;
+
+            // final String productName = rs.getString("productName");

Review comment:
       If we need to keep the native SQL approach, we shall probably fix the 
comments before merging.

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java
##########
@@ -87,6 +88,12 @@ void processPostActiveActions(SavingsAccount account, 
DateTimeFormatter fmt, Set
 
     void postInterest(SavingsAccount account, boolean postInterestAs, 
LocalDate transactionDate, boolean backdatedTxnsAllowedTill);
 
+    // SavingsAccountData postInterest(SavingsAccountData account, boolean 
postInterestAs, LocalDate transactionDate,
+    // boolean backdatedTxnsAllowedTill);

Review comment:
       Do we need these commented lines? Thanks!

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataComparator.java
##########
@@ -0,0 +1,44 @@
+/**
+ * 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.domain;
+
+import java.util.Comparator;
+import 
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+
+public class SavingsAccountTransactionDataComparator implements 
Comparator<SavingsAccountTransactionData> {
+
+    @Override
+    public int compare(final SavingsAccountTransactionData o1, final 
SavingsAccountTransactionData o2) {
+        int compareResult = 0;
+
+        final int comparsion = 
o1.getTransactionDate().compareTo(o2.getLastTransactionDate());
+        if (comparsion == 0) {
+            compareResult = 
o1.getSubmittedOnDate().compareTo(o2.getSubmittedOnDate());
+            if (compareResult == 0 && o1.getId() != null && o2.getId() != 
null) {
+                compareResult = o1.getId().compareTo(o2.getId());
+            } else {
+                compareResult = comparsion;
+            }
+        } else {
+            compareResult = comparsion;
+        }
+        return compareResult;
+    }

Review comment:
       Do you think we could replace this whole thing with something like:
   
   
`Comparator.comparing(SavingsAccountTransactionData::getTransactionDate).thenComparing(SavingsAccountTransactionData::getSubmittedOnDate).thenComparing(SavingsAccountTransactionData::getId);`

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
##########
@@ -77,38 +108,61 @@ public void setTenant(FineractPlatformTenant tenant) {
         this.tenant = tenant;
     }
 
+    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    public void setTransactionTemplate(TransactionTemplate 
transactionTemplate) {
+        this.transactionTemplate = transactionTemplate;
+    }
+
+    public void setResolutionHelper(ResolutionHelper resolutionHelper) {
+        this.resolutionHelper = resolutionHelper;
+    }
+
+    public void setStrategyProvider(CommandStrategyProvider 
commandStrategyProvider) {
+        this.strategyProvider = commandStrategyProvider;
+    }
+
     @Override
     @SuppressFBWarnings(value = {
             "DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for 
random object created and used only once")
     public Void call() throws 
org.apache.fineract.infrastructure.jobs.exception.JobExecutionException {
         ThreadLocalContextUtil.setTenant(tenant);
         Integer maxNumberOfRetries = 
tenant.getConnection().getMaxRetriesOnDeadlock();
         Integer maxIntervalBetweenRetries = 
tenant.getConnection().getMaxIntervalBetweenRetries();
-        final boolean backdatedTxnsAllowedTill = 
this.configurationDomainService.retrievePivotDateConfig();
+
+        // List<BatchResponse> responseList = new ArrayList<>();

Review comment:
       I assume this was left here accidentally.

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
##########
@@ -156,6 +160,8 @@
     private final StandingInstructionRepository standingInstructionRepository;
     private final BusinessEventNotifierService businessEventNotifierService;
     private final GSIMRepositoy gsimRepository;
+    private final JdbcTemplate jdbcTemplate;

Review comment:
       I guess we don't need the JdbcTemplate in this class (?)

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
##########
@@ -230,6 +246,495 @@ public SavingsAccountData retrieveOne(final Long 
accountId) {
         }
     }
 
+    @Override
+    public List<SavingsAccountTransactionData> 
retrieveAllTransactionData(final List<String> refNo) throws DataAccessException 
{
+        String inSql = String.join(",", Collections.nCopies(refNo.size(), 
"?"));
+        String sql = "select " + 
this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in 
(%s)";
+        Object[] params = new Object[refNo.size()];
+        int i = 0;
+        for (String element : refNo) {
+            params[i] = element;
+            i++;
+        }
+        return this.jdbcTemplate.query(String.format(sql, inSql), 
this.savingsAccountTransactionsForBatchMapper, params);
+    }
+
+    @Override
+    public List<SavingsAccountData> 
retrieveAllSavingsDataForInterestPosting(final boolean 
backdatedTxnsAllowedTill, final int pageSize,
+            final Integer status, final Long maxSavingsId) {
+        LocalDate currentDate = DateUtils.getLocalDateOfTenant().minusDays(1);
+
+        String sql = "select " + 
this.savingAccountMapperForInterestPosting.schema()
+                + "join (select a.id from m_savings_account a where a.id > ? 
and a.status_enum = ? limit ?) b on b.id = sa.id ";
+        if (backdatedTxnsAllowedTill) {
+            sql = sql
+                    + "where if (sa.interest_posted_till_date is not null, 
tr.transaction_date >= sa.interest_posted_till_date, tr.transaction_date >= 
sa.activatedon_date) ";
+        }
+
+        sql = sql + "and apm.product_type=2 and sa.interest_posted_till_date<" 
+ java.sql.Date.valueOf(currentDate);
+        sql = sql + " order by sa.id, tr.transaction_date, tr.created_date, 
tr.id";
+
+        List<SavingsAccountData> savingsAccountDataList = 
this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting,
+                new Object[] { maxSavingsId, status, pageSize });
+        for (SavingsAccountData savingsAccountData : savingsAccountDataList) {
+            this.savingAccountAssembler.assembleSavings(savingsAccountData);
+        }
+        return savingsAccountDataList;
+    }
+
+    private static final class SavingAccountMapperForInterestPosting 
implements ResultSetExtractor<List<SavingsAccountData>> {
+
+        private final String schemaSql;
+
+        SavingAccountMapperForInterestPosting() {
+            final StringBuilder sqlBuilder = new StringBuilder(400);
+            sqlBuilder.append("sa.id as id, sa.account_no as accountNo, 
sa.external_id as externalId, ");
+            sqlBuilder.append("sa.deposit_type_enum as depositType, ");
+            sqlBuilder.append("c.id as clientId, c.office_id as 
clientOfficeId, ");
+            sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, 
");

Review comment:
       Probably this won't be needed if we can switch the query above to JPQL.

##########
File path: 
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
##########
@@ -0,0 +1,564 @@
+/**
+ * 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.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
+import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.savings.DepositAccountType;
+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.SavingsAccountData;
+import 
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import 
org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData;
+import 
org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionDataComparator;
+import org.apache.fineract.portfolio.savings.domain.SavingsHelper;
+import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod;
+import org.apache.fineract.portfolio.tax.data.TaxComponentData;
+import org.apache.fineract.portfolio.tax.service.TaxUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SavingsAccountInterestPostingServiceImpl implements 
SavingsAccountInterestPostingService {
+
+    private final SavingsHelper savingsHelper;
+
+    @Autowired
+    public SavingsAccountInterestPostingServiceImpl(final SavingsHelper 
savingsHelper) {
+        this.savingsHelper = savingsHelper;
+    }
+
+    @Override
+    public SavingsAccountData postInterest(final MathContext mc, final 
LocalDate interestPostingUpToDate, final boolean isInterestTransfer,
+            final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final 
Integer financialYearBeginningMonth,
+            final LocalDate postInterestOnDate, final boolean 
backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) {
+
+        Money interestPostedToDate = Money.zero(savingsAccountData.currency());
+        LocalDate startInterestDate = 
getStartInterestCalculationDate(savingsAccountData);
+
+        if (backdatedTxnsAllowedTill && 
savingsAccountData.getSummary().getInterestPostedTillDate() != null) {
+            interestPostedToDate = Money.of(savingsAccountData.currency(), 
savingsAccountData.getSummary().getTotalInterestPosted());
+            SavingsAccountTransactionData savingsAccountTransactionData = 
retrieveLastTransactions(savingsAccountData);
+            Date lastTransactionDate = Date.from(
+                    
savingsAccountTransactionData.getLastTransactionDate().atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant());
+            
savingsAccountData.setStartInterestCalculationDate(lastTransactionDate);
+        } else {
+            savingsAccountData.setStartInterestCalculationDate(
+                    
Date.from(startInterestDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()));
+        }
+
+        final List<PostingPeriod> postingPeriods = calculateInterestUsing(mc, 
interestPostingUpToDate, isInterestTransfer,
+                isSavingsInterestPostingAtCurrentPeriodEnd, 
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
+                savingsAccountData);
+
+        boolean recalucateDailyBalanceDetails = false;
+        boolean applyWithHoldTax = 
isWithHoldTaxApplicableForInterestPosting(savingsAccountData);
+        final List<SavingsAccountTransactionData> withholdTransactions = new 
ArrayList<>();
+
+        
withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig(savingsAccountData));
+
+        for (final PostingPeriod interestPostingPeriod : postingPeriods) {
+
+            final LocalDate interestPostingTransactionDate = 
interestPostingPeriod.dateOfPostingTransaction();
+            final Money interestEarnedToBePostedForPeriod = 
interestPostingPeriod.getInterestEarned();
+
+            if 
(!interestPostingTransactionDate.isAfter(interestPostingUpToDate)) {
+                interestPostedToDate = 
interestPostedToDate.plus(interestEarnedToBePostedForPeriod);
+
+                final SavingsAccountTransactionData postingTransaction = 
findInterestPostingTransactionFor(interestPostingTransactionDate,
+                        savingsAccountData);
+
+                if (postingTransaction == null) {
+                    SavingsAccountTransactionData newPostingTransaction;
+                    if 
(interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.currency())))
 {
+                        newPostingTransaction = 
SavingsAccountTransactionData.interestPosting(savingsAccountData,
+                                interestPostingTransactionDate, 
interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting());
+                    } else {
+                        newPostingTransaction = 
SavingsAccountTransactionData.interestPosting(savingsAccountData,
+                                interestPostingTransactionDate, 
interestEarnedToBePostedForPeriod.negated(),
+                                interestPostingPeriod.isUserPosting());
+                    }
+
+                    
savingsAccountData.updateTransactions(newPostingTransaction);
+
+                    if (applyWithHoldTax) {
+                        
createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), 
interestPostingTransactionDate,
+                                savingsAccountData);
+                    }
+                    recalucateDailyBalanceDetails = true;
+                } else {
+                    boolean correctionRequired = false;
+                    if (postingTransaction.isInterestPostingAndNotReversed()) {
+                        correctionRequired = 
postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod);
+                    } else {
+                        correctionRequired = 
postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated());
+                    }
+                    if (correctionRequired) {
+                        boolean applyWithHoldTaxForOldTransaction = false;
+                        postingTransaction.reverse();
+
+                        final SavingsAccountTransactionData 
withholdTransaction = findTransactionFor(interestPostingTransactionDate,
+                                withholdTransactions);
+
+                        if (withholdTransaction != null) {
+                            withholdTransaction.reverse();
+                            applyWithHoldTaxForOldTransaction = true;
+                        }
+                        SavingsAccountTransactionData newPostingTransaction;
+                        if 
(interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.currency())))
 {
+                            newPostingTransaction = 
SavingsAccountTransactionData.interestPosting(savingsAccountData,
+                                    interestPostingTransactionDate, 
interestEarnedToBePostedForPeriod,
+                                    interestPostingPeriod.isUserPosting());
+                        } else {
+                            newPostingTransaction = 
SavingsAccountTransactionData.overdraftInterest(savingsAccountData,
+                                    interestPostingTransactionDate, 
interestEarnedToBePostedForPeriod.negated(),
+                                    interestPostingPeriod.isUserPosting());
+                        }
+
+                        
savingsAccountData.updateTransactions(newPostingTransaction);
+
+                        if (applyWithHoldTaxForOldTransaction) {
+                            
createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), 
interestPostingTransactionDate,
+                                    savingsAccountData);
+                        }
+                        recalucateDailyBalanceDetails = true;
+                    }
+                }
+            }
+        }
+
+        if (recalucateDailyBalanceDetails) {
+            // no openingBalance concept supported yet but probably will to
+            // allow
+            // for migrations.
+            Money openingAccountBalance = 
Money.zero(savingsAccountData.currency());
+
+            if (backdatedTxnsAllowedTill) {
+                if 
(savingsAccountData.getSummary().getLastInterestCalculationDate() == null) {
+                    openingAccountBalance = 
Money.zero(savingsAccountData.currency());
+                } else {
+                    openingAccountBalance = 
Money.of(savingsAccountData.currency(),
+                            
savingsAccountData.getSummary().getRunningBalanceOnPivotDate());
+                }
+            }
+
+            // update existing transactions so derived balance fields are
+            // correct.
+            recalculateDailyBalances(openingAccountBalance, 
interestPostingUpToDate, backdatedTxnsAllowedTill, savingsAccountData);
+        }
+
+        if (!backdatedTxnsAllowedTill) {
+            
savingsAccountData.getSummary().updateSummary(savingsAccountData.currency(),
+                    
savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), 
savingsAccountData.getSavingsAccountTransactionData());
+        } else {
+            
savingsAccountData.getSummary().updateSummaryWithPivotConfig(savingsAccountData.currency(),
+                    
savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), null,
+                    savingsAccountData.getSavingsAccountTransactionData());
+        }
+
+        return savingsAccountData;
+    }
+
+    protected SavingsAccountTransactionData findTransactionFor(final LocalDate 
postingDate,
+            final List<SavingsAccountTransactionData> transactions) {
+        SavingsAccountTransactionData transaction = null;
+        for (final SavingsAccountTransactionData savingsAccountTransaction : 
transactions) {
+            if (savingsAccountTransaction.occursOn(postingDate)) {
+                transaction = savingsAccountTransaction;
+                break;
+            }
+        }
+        return transaction;
+    }
+
+    public List<PostingPeriod> calculateInterestUsing(final MathContext mc, 
final LocalDate upToInterestCalculationDate,
+            boolean isInterestTransfer, final boolean 
isSavingsInterestPostingAtCurrentPeriodEnd, final Integer 
financialYearBeginningMonth,
+            final LocalDate postInterestOnDate, final boolean 
backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) {
+
+        // no openingBalance concept supported yet but probably will to allow
+        // for migrations.
+        Money openingAccountBalance = null;
+
+        // Check global configurations and 'pivot' date is null
+        if (backdatedTxnsAllowedTill) {
+            openingAccountBalance = Money.of(savingsAccountData.currency(), 
savingsAccountData.getSummary().getRunningBalanceOnPivotDate());
+        } else {
+            openingAccountBalance = Money.zero(savingsAccountData.currency());
+        }
+
+        // update existing transactions so derived balance fields are
+        // correct.
+        recalculateDailyBalances(openingAccountBalance, 
upToInterestCalculationDate, backdatedTxnsAllowedTill, savingsAccountData);
+
+        // 1. default to calculate interest based on entire history OR
+        // 2. determine latest 'posting period' and find interest credited to
+        // that period
+
+        // A generate list of EndOfDayBalances (not including interest 
postings)
+
+        final SavingsPostingInterestPeriodType postingPeriodType = 
SavingsPostingInterestPeriodType
+                .fromInt(savingsAccountData.getInterestPostingPeriodType());
+
+        final SavingsCompoundingInterestPeriodType compoundingPeriodType = 
SavingsCompoundingInterestPeriodType
+                
.fromInt(savingsAccountData.getInterestCompoundingPeriodType());
+
+        final SavingsInterestCalculationDaysInYearType daysInYearType = 
SavingsInterestCalculationDaysInYearType
+                
.fromInt(savingsAccountData.getInterestCalculationDaysInYearType());
+
+        List<LocalDate> postedAsOnDates = 
getManualPostingDates(savingsAccountData);
+        if (postInterestOnDate != null) {
+            postedAsOnDates.add(postInterestOnDate);
+        }
+        final List<LocalDateInterval> postingPeriodIntervals = 
this.savingsHelper.determineInterestPostingPeriods(
+                savingsAccountData.getStartInterestCalculationDate(), 
upToInterestCalculationDate, postingPeriodType,
+                financialYearBeginningMonth, postedAsOnDates);
+
+        final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
+
+        Money periodStartingBalance;
+        if (savingsAccountData.getStartInterestCalculationDate() != null) {
+            final SavingsAccountTransactionData transaction = 
retrieveLastTransactions(savingsAccountData);
+
+            if (transaction == null) {
+                final String defaultUserMessage = "No transactions were found 
on the specified date "
+                        + 
savingsAccountData.getStartInterestCalculationDate().toString() + " for account 
number "
+                        + savingsAccountData.getAccountNo() + " and resource 
id " + savingsAccountData.getId();
+
+                final ApiParameterError error = 
ApiParameterError.parameterError(
+                        
"error.msg.savingsaccount.transaction.incorrect.start.interest.calculation.date",
 defaultUserMessage,
+                        "transactionDate", 
savingsAccountData.getStartInterestCalculationDate().toString());
+
+                final List<ApiParameterError> dataValidationErrors = new 
ArrayList<>();
+                dataValidationErrors.add(error);
+
+                throw new 
PlatformApiDataValidationException(dataValidationErrors);
+            }
+
+            periodStartingBalance = 
transaction.getRunningBalance(savingsAccountData.currency());
+        } else {
+            periodStartingBalance = Money.zero(savingsAccountData.currency());
+        }
+
+        final SavingsInterestCalculationType interestCalculationType = 
SavingsInterestCalculationType
+                .fromInt(savingsAccountData.getInterestCalculationType());
+        final BigDecimal interestRateAsFraction = 
getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate, 
savingsAccountData);
+        final BigDecimal overdraftInterestRateAsFraction = 
getEffectiveOverdraftInterestRateAsFraction(mc, savingsAccountData);
+        final Collection<Long> interestPostTransactions = 
this.savingsHelper.fetchPostInterestTransactionIds(savingsAccountData.getId());
+        final Money minBalanceForInterestCalculation = 
Money.of(savingsAccountData.currency(),
+                minBalanceForInterestCalculation(savingsAccountData));
+        final Money minOverdraftForInterestCalculation = 
Money.of(savingsAccountData.currency(),
+                savingsAccountData.getMinOverdraftForInterestCalculation());
+        final MonetaryCurrency monetaryCurrency = 
MonetaryCurrency.fromCurrencyData(savingsAccountData.currency());
+
+        for (final LocalDateInterval periodInterval : postingPeriodIntervals) {
+
+            boolean isUserPosting = false;
+            if 
(postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) {
+                isUserPosting = true;
+            }
+            final PostingPeriod postingPeriod = 
PostingPeriod.createFromDTO(periodInterval, periodStartingBalance,
+                    
retreiveOrderedNonInterestPostingTransactions(savingsAccountData), 
monetaryCurrency, compoundingPeriodType,
+                    interestCalculationType, interestRateAsFraction, 
daysInYearType.getValue(), upToInterestCalculationDate,
+                    interestPostTransactions, isInterestTransfer, 
minBalanceForInterestCalculation,
+                    isSavingsInterestPostingAtCurrentPeriodEnd, 
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
+                    isUserPosting, financialYearBeginningMonth);
+
+            periodStartingBalance = postingPeriod.closingBalance();
+
+            allPostingPeriods.add(postingPeriod);
+        }
+
+        
this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, 
allPostingPeriods,
+                getLockedInUntilLocalDate(savingsAccountData), false);
+
+        
savingsAccountData.getSummary().updateFromInterestPeriodSummaries(monetaryCurrency,
 allPostingPeriods);
+
+        if (backdatedTxnsAllowedTill) {
+            
savingsAccountData.getSummary().updateSummaryWithPivotConfig(savingsAccountData.currency(),
+                    
savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), null, 
savingsAccountData.getTransactions());
+        } else {
+            
savingsAccountData.getSummary().updateSummary(savingsAccountData.currency(),
+                    
savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), 
savingsAccountData.getTransactions());
+        }
+
+        return allPostingPeriods;
+    }
+
+    private List<SavingsAccountTransactionData> 
retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData 
savingsAccountData) {
+        final List<SavingsAccountTransactionData> listOfTransactionsSorted = 
retreiveListOfTransactions(savingsAccountData);
+
+        final List<SavingsAccountTransactionData> 
orderedNonInterestPostingTransactions = new ArrayList<>();
+
+        for (final SavingsAccountTransactionData transaction : 
listOfTransactionsSorted) {
+            if (!(transaction.isInterestPostingAndNotReversed() || 
transaction.isOverdraftInterestAndNotReversed())
+                    && transaction.isNotReversed()) {
+                orderedNonInterestPostingTransactions.add(transaction);
+            }
+        }
+        orderedNonInterestPostingTransactions.sort(new 
SavingsAccountTransactionDataComparator());
+        return orderedNonInterestPostingTransactions;
+    }
+
+    private List<SavingsAccountTransactionData> 
retreiveListOfTransactions(final SavingsAccountData savingsAccountData) {
+        if (savingsAccountData.getTransactions() != null && 
savingsAccountData.getTransactions().size() == 1) {
+            return savingsAccountData.getTransactions();
+        }
+
+        final List<SavingsAccountTransactionData> listOfTransactionsSorted = 
new ArrayList<>();
+        listOfTransactionsSorted.addAll(savingsAccountData.getTransactions());
+
+        final SavingsAccountTransactionDataComparator transactionComparator = 
new SavingsAccountTransactionDataComparator();
+        Collections.sort(listOfTransactionsSorted, transactionComparator);
+        return listOfTransactionsSorted;
+    }
+
+    protected LocalDate getLockedInUntilLocalDate(final SavingsAccountData 
savingsAccount) {
+        LocalDate lockedInUntilLocalDate = null;
+        if (savingsAccount.getLockedInUntilDate() != null) {
+            lockedInUntilLocalDate = savingsAccount.getActivationLocalDate();
+            // lockedInUntilLocalDate = 
LocalDate.ofInstant(this.lockedInUntilDate.toInstant(),
+            // DateUtils.getDateTimeZoneOfTenant());
+        }
+        return lockedInUntilLocalDate;
+    }
+
+    private BigDecimal minBalanceForInterestCalculation(final 
SavingsAccountData savingsAccountData) {
+        return savingsAccountData.getMinBalanceForInterestCalculation();
+    }
+
+    private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext 
mc, final SavingsAccountData savingsAccountData) {
+        return 
savingsAccountData.getNominalAnnualInterestRateOverdraft().divide(BigDecimal.valueOf(100L),
 mc);
+    }
+
+    @SuppressWarnings("unused")
+    private BigDecimal getEffectiveInterestRateAsFraction(final MathContext 
mc, final LocalDate upToInterestCalculationDate,
+            final SavingsAccountData savingsAccountData) {
+        return 
savingsAccountData.getNominalAnnualInterestRate().divide(BigDecimal.valueOf(100L),
 mc);
+    }
+
+    public List<SavingsAccountTransactionData> getTransactions(final 
SavingsAccountData savingsAccountData) {
+        return savingsAccountData.getTransactions();
+    }
+
+    private SavingsAccountTransactionData retrieveLastTransactions(final 
SavingsAccountData savingsAccountData) {
+        if (savingsAccountData.getTransactions() != null && 
savingsAccountData.getTransactions().size() == 1) {
+            return savingsAccountData.getTransactions().get(0);
+        }
+        final List<SavingsAccountTransactionData> listOfTransactionsSorted = 
new ArrayList<>();
+        listOfTransactionsSorted.addAll(savingsAccountData.getTransactions());
+        final SavingsAccountTransactionDataComparator transactionComparator = 
new SavingsAccountTransactionDataComparator();
+        Collections.sort(listOfTransactionsSorted, transactionComparator);
+        return listOfTransactionsSorted.get(0);
+    }
+
+    public LocalDate getStartInterestCalculationDate(final SavingsAccountData 
savingsAccountData) {
+        LocalDate startInterestCalculationLocalDate = null;
+        if (savingsAccountData.getStartInterestCalculationDate() != null) {
+            startInterestCalculationLocalDate = 
savingsAccountData.getStartInterestCalculationDate();
+        } else {
+            startInterestCalculationLocalDate = 
getActivationLocalDate(savingsAccountData);
+        }
+        return startInterestCalculationLocalDate;
+    }
+
+    public LocalDate getActivationLocalDate(final SavingsAccountData 
savingsAccountData) {
+        LocalDate activationLocalDate = null;
+        if (savingsAccountData.getActivationLocalDate() != null) {
+            activationLocalDate = savingsAccountData.getActivationLocalDate();
+        }
+        return activationLocalDate;
+    }
+
+    public List<LocalDate> getManualPostingDates(final SavingsAccountData 
savingsAccountData) {
+        List<LocalDate> transactions = new ArrayList<>();
+        for (SavingsAccountTransactionData trans : 
savingsAccountData.getSavingsAccountTransactionData()) {
+            if (trans.isInterestPosting() && trans.isNotReversed() && 
trans.isManualTransaction()) {
+                transactions.add(trans.getTransactionLocalDate());
+            }
+        }
+        return transactions;
+    }
+
+    protected void recalculateDailyBalances(final Money openingAccountBalance, 
final LocalDate interestPostingUpToDate,
+            final boolean backdatedTxnsAllowedTill, final SavingsAccountData 
savingsAccountData) {
+
+        Money runningBalance = openingAccountBalance.copy();
+
+        List<SavingsAccountTransactionData> accountTransactionsSorted = 
retreiveListOfTransactions(savingsAccountData);
+
+        boolean isTransactionsModified = false;
+
+        for (final SavingsAccountTransactionData transaction : 
accountTransactionsSorted) {
+            if (transaction.isReversed()) {
+                transaction.zeroBalanceFields();
+            } else {
+                Money overdraftAmount = 
Money.zero(savingsAccountData.currency());
+                Money transactionAmount = 
Money.zero(savingsAccountData.currency());
+                if (transaction.isCredit() || transaction.isAmountRelease()) {
+                    if (runningBalance.isLessThanZero()) {
+                        Money diffAmount = 
Money.of(savingsAccountData.currency(), 
transaction.getAmount()).plus(runningBalance);
+                        if (diffAmount.isGreaterThanZero()) {
+                            overdraftAmount = 
Money.of(savingsAccountData.currency(), 
transaction.getAmount()).minus(diffAmount);
+                        } else {
+                            overdraftAmount = 
Money.of(savingsAccountData.currency(), transaction.getAmount());
+                        }
+                    }
+                    transactionAmount = 
transactionAmount.plus(transaction.getAmount());
+                } else if (transaction.isDebit() || 
transaction.isAmountOnHold()) {
+                    if (runningBalance.isLessThanZero()) {
+                        overdraftAmount = 
Money.of(savingsAccountData.currency(), transaction.getAmount());
+                    }
+                    transactionAmount = 
transactionAmount.minus(transaction.getAmount());
+                }
+
+                runningBalance = runningBalance.plus(transactionAmount);
+                transaction.updateRunningBalance(runningBalance);
+                if (overdraftAmount.isZero() && 
runningBalance.isLessThanZero()) {
+                    overdraftAmount = 
overdraftAmount.plus(runningBalance.getAmount().negate());
+                }
+                if (transaction.getId() == null && 
overdraftAmount.isGreaterThanZero()) {
+                    
transaction.updateOverdraftAmount(overdraftAmount.getAmount());
+                } else if 
(overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.currency(), 
transaction.getOverdraftAmount()))) {
+                    SavingsAccountTransactionData accountTransaction = 
SavingsAccountTransactionData.copyTransaction(transaction);
+                    if (transaction.isChargeTransaction()) {
+                        Set<SavingsAccountChargesPaidByData> chargesPaidBy = 
transaction.getSavingsAccountChargesPaid();
+                        final Set<SavingsAccountChargesPaidByData> 
newChargePaidBy = new HashSet<>();
+                        chargesPaidBy.forEach(
+                                x -> 
newChargePaidBy.add(SavingsAccountChargesPaidByData.instance(x.getChargeId(), 
x.getAmount())));
+                        
accountTransaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy);
+                    }
+                    transaction.reverse();
+                    if (overdraftAmount.isGreaterThanZero()) {
+                        
accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount());
+                    }
+                    accountTransaction.updateRunningBalance(runningBalance);
+                    addTransactionToExisting(accountTransaction, 
savingsAccountData);
+
+                    isTransactionsModified = true;
+                }
+
+            }
+        }
+
+        if (isTransactionsModified) {
+            accountTransactionsSorted = 
retreiveListOfTransactions(savingsAccountData);
+        }
+        resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, 
interestPostingUpToDate, savingsAccountData);
+    }
+
+    public void addTransactionToExisting(final SavingsAccountTransactionData 
transaction, final SavingsAccountData savingsAccountData) {
+        savingsAccountData.updateTransactions(transaction);
+    }
+
+    private List<SavingsAccountTransactionData> 
findWithHoldSavingsTransactionsWithPivotConfig(
+            final SavingsAccountData savingsAccountData) {
+        final List<SavingsAccountTransactionData> withholdTransactions = new 
ArrayList<>();
+        List<SavingsAccountTransactionData> trans = 
savingsAccountData.getSavingsAccountTransactionData();
+        for (final SavingsAccountTransactionData transaction : trans) {
+            if (transaction.isWithHoldTaxAndNotReversed()) {
+                withholdTransactions.add(transaction);
+            }
+        }
+        return withholdTransactions;
+    }
+
+    private boolean createWithHoldTransaction(final BigDecimal amount, final 
LocalDate date, final SavingsAccountData savingsAccountData) {
+        boolean isTaxAdded = false;
+        if (savingsAccountData.getTaxGroupData() != null && 
amount.compareTo(BigDecimal.ZERO) > 0) {
+            Map<TaxComponentData, BigDecimal> taxSplit = 
TaxUtils.splitTaxData(amount, date,
+                    
savingsAccountData.getTaxGroupData().getTaxAssociations().stream().collect(Collectors.toSet()),
 amount.scale());
+            BigDecimal totalTax = TaxUtils.totalTaxDataAmount(taxSplit);
+            if (totalTax.compareTo(BigDecimal.ZERO) > 0) {
+                SavingsAccountTransactionData withholdTransaction = 
SavingsAccountTransactionData.withHoldTax(savingsAccountData, date,
+                        Money.of(savingsAccountData.currency(), totalTax), 
taxSplit);
+
+                
savingsAccountData.getSavingsAccountTransactionData().add(withholdTransaction);
+                // if (backdatedTxnsAllowedTill) {

Review comment:
       Probably we should fix this too.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to