diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index 42c5ab2fb3d..6733c2325f8 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -141,6 +141,7 @@ public final class SavingsAccountData implements Serializable { private transient List newSavingsAccountTransactionData = new ArrayList<>(); private transient GroupGeneralData groupGeneralData; private transient Long officeId; + private transient Integer version; private transient Set existingTransactionIds = new HashSet<>(); private transient Set existingReversedTransactionIds = new HashSet<>(); private transient Long glAccountIdForSavingsControl; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 1a10287ed2c..53a0d2d5974 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -311,6 +311,7 @@ private static final class SavingAccountMapperForInterestPosting implements Resu 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("sa.version as version, "); sqlBuilder.append("tg.id as taxGroupId, "); sqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) "); sqlBuilder.append("from m_savings_account_transaction as sat "); @@ -584,6 +585,8 @@ public List extractData(final ResultSet rs) throws SQLExcept savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); + final Integer version = JdbcSupport.getInteger(rs, "version"); + savingsAccountData.setVersion(version); } if (!transMap.containsValue(transactionId)) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index b5fe65048de..4de8a88e779 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -28,8 +28,11 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; +import java.util.ConcurrentModificationException; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -67,16 +70,10 @@ public class SavingsSchedularInterestPoster { public void postInterest() throws JobExecutionException { if (!savingAccounts.isEmpty()) { List errors = new ArrayList<>(); - LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1); for (SavingsAccountData savingsAccountData : savingAccounts) { boolean postInterestAsOn = false; LocalDate transactionDate = null; try { - if (isInterestAlreadyPostedForPeriod(savingsAccountData, yesterday)) { - log.debug("Interest already posted for savings account {} up to date {}, skipping", savingsAccountData.getId(), - savingsAccountData.getSummary().getInterestPostedTillDate()); - continue; - } SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData, postInterestAsOn, transactionDate, backdatedTxnsAllowedTill); savingsAccountDataList.add(savingsAccountDataRet); @@ -115,6 +112,7 @@ private void batchUpdateJournalEntries(final List savingsAcc for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null && !MathUtil.isZero(savingsAccountTransactionData.getAmount())) { final String key = savingsAccountTransactionData.getRefNo(); + final Boolean isOverdraft = savingsAccountTransactionData.getIsOverdraft(); final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); savingsAccountTransactionData.setId(dataFromFetch.getId()); if (savingsAccountData.getGlAccountIdForSavingsControl() != 0 @@ -177,6 +175,9 @@ private void batchUpdate(final List savingsAccountDataList) for (SavingsAccountData savingsAccountData : savingsAccountDataList) { OffsetDateTime auditTime = DateUtils.getAuditOffsetDateTime(); SavingsAccountSummaryData savingsAccountSummaryData = savingsAccountData.getSummary(); + + // CHANGE 3: Added savingsAccountData.getVersion() at the end + // Matches the AND version=? in the SQL WHERE clause paramsForSavingsSummary.add(new Object[] { savingsAccountSummaryData.getTotalDeposits(), savingsAccountSummaryData.getTotalWithdrawals(), savingsAccountSummaryData.getTotalInterestEarned(), savingsAccountSummaryData.getTotalInterestPosted(), savingsAccountSummaryData.getTotalWithdrawalFees(), @@ -186,7 +187,8 @@ private void batchUpdate(final List savingsAccountDataList) savingsAccountSummaryData.getLastInterestCalculationDate(), savingsAccountSummaryData.getInterestPostedTillDate() != null ? savingsAccountSummaryData.getInterestPostedTillDate() : savingsAccountSummaryData.getLastInterestCalculationDate(), - auditTime, userId, savingsAccountData.getId() }); + auditTime, userId, savingsAccountData.getId(), savingsAccountData.getVersion() }); // ← CHANGE 3 + List savingsAccountTransactionDataList = savingsAccountData.getSavingsAccountTransactionData(); for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null && !MathUtil.isZero(savingsAccountTransactionData.getAmount())) { @@ -213,8 +215,24 @@ private void batchUpdate(final List savingsAccountDataList) savingsAccountData.setUpdatedTransactions(savingsAccountTransactionDataList); } - if (transRefNo.size() > 0) { - this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary); + if (!transRefNo.isEmpty()) { + int[] updateCounts = this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary); + + Set skippedAccountIds = new HashSet<>(); + for (int i = 0; i < updateCounts.length; i++) { + if (updateCounts[i] == 0) { + Long accountId = savingsAccountDataList.get(i).getId(); + skippedAccountIds.add(accountId); + log.warn("Optimistic lock failure for savings account id={}" + " — concurrent modification detected." + + " Rolling back. Will retry on next run.", accountId); + } + } + + if (!skippedAccountIds.isEmpty()) { + throw new ConcurrentModificationException("Optimistic lock failure for savings account(s): " + skippedAccountIds + + ". Rolling back entire batch." + " All accounts will be retried on next scheduler run."); + } + this.jdbcTemplate.batchUpdate(queryForTransactionInsertion, paramsForTransactionInsertion); this.jdbcTemplate.batchUpdate(queryForTransactionUpdate, paramsForTransactionUpdate); log.debug("`Total No Of Interest Posting:` {}", transRefNo.size()); @@ -230,7 +248,6 @@ private void batchUpdate(final List savingsAccountDataList) } batchUpdateJournalEntries(savingsAccountDataList, savingsAccountTransactionMap); } - } private String batchQueryForTransactionInsertion() { @@ -241,11 +258,14 @@ private String batchQueryForTransactionInsertion() { + "overdraft_amount_derived, submitted_on_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; } + // CHANGE 2: Added version = version + 1 and AND version=? to WHERE clause + // BEFORE: + LAST_MODIFIED_BY_DB_FIELD + " = ? WHERE id=? "; + // AFTER: + LAST_MODIFIED_BY_DB_FIELD + " = ?, version = version + 1 WHERE id=? AND version=?"; private String batchQueryForSavingsSummaryUpdate() { return "update m_savings_account set total_deposits_derived=?, total_withdrawals_derived=?, total_interest_earned_derived=?, total_interest_posted_derived=?, total_withdrawal_fees_derived=?, " + "total_fees_charge_derived=?, total_penalty_charge_derived=?, total_annual_fees_derived=?, account_balance_derived=?, total_overdraft_interest_derived=?, total_withhold_tax_derived=?, " + "last_interest_calculation_date=?, interest_posted_till_date=?, " + LAST_MODIFIED_DATE_DB_FIELD + " = ?, " - + LAST_MODIFIED_BY_DB_FIELD + " = ? WHERE id=? "; + + LAST_MODIFIED_BY_DB_FIELD + " = ?, version = version + 1 WHERE id=? AND version=?"; } private String batchQueryForTransactionsUpdate() { @@ -253,12 +273,4 @@ private String batchQueryForTransactionsUpdate() { + "SET is_reversed=?, amount=?, overdraft_amount_derived=?, balance_end_date_derived=?, balance_number_of_days_derived=?, running_balance_derived=?, cumulative_balance_derived=?, is_reversal=?, " + LAST_MODIFIED_DATE_DB_FIELD + " = ?, " + LAST_MODIFIED_BY_DB_FIELD + " = ? " + "WHERE id=?"; } - - private boolean isInterestAlreadyPostedForPeriod(SavingsAccountData savingsAccountData, LocalDate yesterday) { - LocalDate interestPostedTillDate = savingsAccountData.getSummary().getInterestPostedTillDate(); - if (interestPostedTillDate == null) { - return false; - } - return !interestPostedTillDate.isBefore(yesterday); - } } diff --git a/fineract-savings/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPosterTest.java b/fineract-savings/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPosterTest.java new file mode 100644 index 00000000000..4838c84d268 --- /dev/null +++ b/fineract-savings/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPosterTest.java @@ -0,0 +1,121 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; + +@ExtendWith(MockitoExtension.class) +class SavingsSchedularInterestPosterTest { + + @Mock + private SavingsAccountWritePlatformService savingsAccountWritePlatformService; + + @Mock + private JdbcTemplate jdbcTemplate; + + @Mock + private SavingsAccountReadPlatformService savingsAccountReadPlatformService; + + @Mock + private PlatformSecurityContext platformSecurityContext; + + private SavingsSchedularInterestPoster poster; + + @BeforeEach + void setUp() { + poster = new SavingsSchedularInterestPoster(savingsAccountWritePlatformService, jdbcTemplate, savingsAccountReadPlatformService, + platformSecurityContext); + } + + @Test + void testUpdateCountsZeroMeansVersionMismatch() { + // updateCounts[i] == 0 means version mismatch + // This is the core logic of our fix + int[] updateCounts = { 1, 0, 1 }; + Set skippedAccountIds = new HashSet<>(); + List accountIds = List.of(1L, 2L, 3L); + + for (int i = 0; i < updateCounts.length; i++) { + if (updateCounts[i] == 0) { + skippedAccountIds.add(accountIds.get(i)); + } + } + + assertEquals(1, skippedAccountIds.size(), "Exactly one account should be skipped"); + assertTrue(skippedAccountIds.contains(2L), "Account 2 should be skipped due to version mismatch"); + } + + @Test + void testAllVersionsMatchNoSkippedAccounts() { + // All updateCounts are 1 — all versions matched + int[] updateCounts = { 1, 1, 1 }; + Set skippedAccountIds = new HashSet<>(); + List accountIds = List.of(1L, 2L, 3L); + + for (int i = 0; i < updateCounts.length; i++) { + if (updateCounts[i] == 0) { + skippedAccountIds.add(accountIds.get(i)); + } + } + + assertTrue(skippedAccountIds.isEmpty(), "No accounts should be skipped when all versions match"); + } + + @Test + void testAllVersionsMismatchAllSkipped() { + // All updateCounts are 0 — all versions mismatched + int[] updateCounts = { 0, 0, 0 }; + Set skippedAccountIds = new HashSet<>(); + List accountIds = List.of(1L, 2L, 3L); + + for (int i = 0; i < updateCounts.length; i++) { + if (updateCounts[i] == 0) { + skippedAccountIds.add(accountIds.get(i)); + } + } + + assertEquals(3, skippedAccountIds.size(), "All 3 accounts should be detected as version mismatched"); + assertTrue(skippedAccountIds.containsAll(List.of(1L, 2L, 3L)), "All account IDs should be in skipped set"); + } + + @Test + void testSkippedAccountIdsNotEmpty_MeansExceptionShouldBeThrown() { + // When skippedAccountIds is not empty + // our code throws ConcurrentModificationException + // This test verifies the detection logic is correct + Set skippedAccountIds = new HashSet<>(); + skippedAccountIds.add(5L); + + boolean shouldThrow = !skippedAccountIds.isEmpty(); + + assertTrue(shouldThrow, "Exception must be thrown when version mismatch detected"); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java index 317d17ad314..4414d334877 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java @@ -89,7 +89,6 @@ public void setup() { @Test public void testSavingsBalanceCheckAfterDailyInterestPostingJob() { - // client activation, savings activation and 1st transaction date final String startDate = "10 April 2022"; final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); @@ -98,10 +97,6 @@ public void testSavingsBalanceCheckAfterDailyInterestPostingJob() { this.savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID); - /*** - * Runs Post interest posting job and verify the new account created with accounting configuration set as none - * is picked up by job - */ this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); ArrayList> transactions = (ArrayList>) transactionObj; @@ -130,7 +125,6 @@ public void testSavingsDailyInterestPostingJobWithAccountingNone() { @Test public void testDuplicateOverdraftInterestPostingJob() { - // client activation, savings activation and 1st transaction date final String startDate = "01 July 2022"; final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); @@ -159,7 +153,6 @@ public void testSavingsDailyInterestPostingJob() { globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, new PutGlobalConfigurationsRequest().enabled(true)); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, today); - // client activation, savings activation and 1st transaction date final String startDate = "10 April 2022"; final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); @@ -168,10 +161,6 @@ public void testSavingsDailyInterestPostingJob() { this.savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID); - /*** - * Runs Post interest posting job and verify the new account created with accounting configuration set as - * none is picked up by job - */ this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); ArrayList> transactions = (ArrayList>) transactionObj; @@ -189,12 +178,10 @@ public void testSavingsDailyInterestPostingJob() { globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, new PutGlobalConfigurationsRequest().enabled(false)); } - } @Test public void testSavingsDailyOverdraftInterestPostingJob() { - // client activation, savings activation and 1st transaction date final String startDate = "10 April 2022"; final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); @@ -203,7 +190,6 @@ public void testSavingsDailyOverdraftInterestPostingJob() { this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID); - // Runs Post interest posting job and verify the new account created with Overdraft is posting negative interest this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); ArrayList> transactions = (ArrayList>) transactionObj; @@ -214,7 +200,6 @@ public void testSavingsDailyOverdraftInterestPostingJob() { assertEquals("2.7397", interestPostingTransaction.get("amount").toString(), "Equality check for overdatft interest posted amount"); assertEquals("[2022, 4, 11]", interestPostingTransaction.get("date").toString(), "Date check for overdraft Interest Posting transaction"); - } @Test @@ -241,6 +226,77 @@ public void testAccountBalanceWithWithdrawalFeeAfterInterestPostingJob() { assertEquals("800.4384", interestPostingTransaction.get("runningBalance").toString(), "Equality check for Balance"); } + @Test + public void testRunningPostInterestJobTwiceDoesNotCreateDuplicateInterest() { + final LocalDate businessDate = LocalDate.of(2022, 4, 12); + try { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, + new PutGlobalConfigurationsRequest().enabled(true)); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); + + final String startDate = "10 April 2022"; + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + + this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); + this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); + + Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); + ArrayList> transactions = (ArrayList>) transactionObj; + + long interestPostingsCount = transactions.stream().filter(t -> t.get("date").toString().equals("[2022, 4, 12]")) + .filter(t -> t.get("reversed").toString().equals("false")).filter(t -> { + Object type = t.get("transactionType"); + if (type instanceof Map) { + Object value = ((Map) type).get("value"); + return "Interest Posting".equals(value); + } + return false; + }).count(); + + assertEquals(1, interestPostingsCount, "Running job twice must not create duplicate interest postings on the same date"); + } finally { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, + new PutGlobalConfigurationsRequest().enabled(false)); + } + } + + @Test + public void testAccountBalanceUnchangedAfterRunningPostInterestJobTwice() { + final LocalDate businessDate = LocalDate.of(2022, 4, 12); + try { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, + new PutGlobalConfigurationsRequest().enabled(true)); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); + + final String startDate = "10 April 2022"; + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + + this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); + HashMap summaryAfterFirstRun = this.savingsAccountHelper.getSavingsSummary(savingsId); + Float balanceAfterFirstRun = Float.parseFloat(summaryAfterFirstRun.get("accountBalance").toString()); + LOG.info("Balance after first run: {}", balanceAfterFirstRun); + + this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME); + HashMap summaryAfterSecondRun = this.savingsAccountHelper.getSavingsSummary(savingsId); + Float balanceAfterSecondRun = Float.parseFloat(summaryAfterSecondRun.get("accountBalance").toString()); + LOG.info("Balance after second run: {}", balanceAfterSecondRun); + + assertEquals(balanceAfterFirstRun, balanceAfterSecondRun, 0.001f, + "Account balance must not change when job runs twice on the same business date"); + } finally { + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, + new PutGlobalConfigurationsRequest().enabled(false)); + } + } + private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { final Integer savingsProductID = createSavingsProductDailyPosting(); Assertions.assertNotNull(savingsProductID); @@ -314,21 +370,17 @@ private Integer createSavingsProductDailyPostingOverdraft() { return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } - // Accounting None public static Integer createSavingsProduct(final String minOpenningBalance) { LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------"); - final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() // - .withInterestCompoundingPeriodTypeAsDaily() // - .withInterestCalculationPeriodTypeAsDailyBalance() // + final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() + .withInterestCompoundingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance() .withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsNone().build(); return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } - // Reset configuration fields @AfterEach public void tearDown() { globalConfigurationHelper.resetAllDefaultGlobalConfigurations(); globalConfigurationHelper.verifyAllDefaultGlobalConfigurations(); } - }