You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by GitBox <gi...@apache.org> on 2022/02/04 18:29:32 UTC

[GitHub] [fineract] galovics commented on a change in pull request #2025: Improve performance of Interest Posting Job

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: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org