diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index dbc354737..83617b084 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -180,7 +180,7 @@ public final class SavingsAccountData implements Serializable { this.reasonForBlock = null; this.timeline = null; this.currency = null; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; + this.nominalAnnualInterestRate = nominalAnnualInterestRate != null ? nominalAnnualInterestRate : BigDecimal.ZERO; this.interestCompoundingPeriodType = interestCompoundingPeriodType; this.interestPostingPeriodType = interestPostingPeriodType; this.interestCalculationType = interestCalculationType; @@ -537,7 +537,7 @@ public final class SavingsAccountData implements Serializable { this.reasonForBlock = null; this.timeline = null; this.currency = null; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; + this.nominalAnnualInterestRate = nominalAnnualInterestRate == null ? BigDecimal.ZERO : nominalAnnualInterestRate; this.interestCompoundingPeriodType = interestCompoundingPeriodType; this.interestPostingPeriodType = interestPostingPeriodType; this.interestCalculationType = interestCalculationType; @@ -959,7 +959,7 @@ public final class SavingsAccountData implements Serializable { this.reasonForBlock = reasonForBlock; this.timeline = timeline; this.currency = currency; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; + this.nominalAnnualInterestRate = nominalAnnualInterestRate == null ? BigDecimal.ZERO : nominalAnnualInterestRate; this.interestCompoundingPeriodType = interestPeriodType; this.interestPostingPeriodType = interestPostingPeriodType; this.interestCalculationType = interestCalculationType; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java index fbbe5cccb..2cb810b15 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java @@ -78,7 +78,7 @@ public class SavingsAccountSummaryData implements Serializable { this.interestNotPosted = interestNotPosted; this.lastInterestCalculationDate = lastInterestCalculationDate; this.availableBalance = availableBalance; - this.interestPostedTillDate = interestPostedTillDate; + this.interestPostedTillDate = interestPostedTillDate == null ? lastInterestCalculationDate : interestPostedTillDate; } public void setPrevInterestPostedTillDate(LocalDate interestPostedTillDate) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java index dd660ffad..74a19d350 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java @@ -108,7 +108,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); } else { - newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, + newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), interestPostingPeriod.isUserPosting()); } @@ -250,7 +250,8 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI final List allPostingPeriods = new ArrayList<>(); Money periodStartingBalance; - if (savingsAccountData.getStartInterestCalculationDate() != null) { + if (savingsAccountData.getStartInterestCalculationDate() != null + && !savingsAccountData.getStartInterestCalculationDate().equals(savingsAccountData.getActivationLocalDate())) { final SavingsAccountTransactionData transaction = retrieveLastTransactions(savingsAccountData); if (transaction == null) { @@ -361,7 +362,9 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI } private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc, final SavingsAccountData savingsAccountData) { - return savingsAccountData.getNominalAnnualInterestRateOverdraft().divide(BigDecimal.valueOf(100L), mc); + return savingsAccountData.getNominalAnnualInterestRateOverdraft() != null + ? savingsAccountData.getNominalAnnualInterestRateOverdraft().divide(BigDecimal.valueOf(100L), mc) + : BigDecimal.ZERO; } @SuppressWarnings("unused") 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 f75dc60aa..7e98984e9 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 @@ -274,7 +274,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead + "where (CASE WHEN sa.interest_posted_till_date is not null THEN tr.transaction_date >= sa.interest_posted_till_date ELSE tr.transaction_date >= sa.activatedon_date END) "; } - sql = sql + "and apm.product_type=2 and sa.interest_posted_till_date < '" + java.sql.Date.valueOf(currentDate) + "'"; + sql = sql + " and (sa.interest_posted_till_date is null or 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 savingsAccountDataList = this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting, // NOSONAR diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index b78a39bfe..ae896230f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -359,7 +359,7 @@ public class SavingsSchedularInterestPoster implements Callable { query.append("transaction_type_enum, transaction_date, amount, balance_end_date_derived,"); query.append("balance_number_of_days_derived, running_balance_derived, cumulative_balance_derived,"); query.append("created_date, appuser_id, is_manual, is_loan_disbursement, ref_no) VALUES "); - query.append("(?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)"); + query.append("(?, ?, false, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, ?)"); return query.toString(); } 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 new file mode 100644 index 000000000..d92213c06 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java @@ -0,0 +1,171 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.SchedulerJobHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SavingsInterestPostingJobIntegrationTest { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsInterestPostingJobIntegrationTest.class); + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private SavingsProductHelper savingsProductHelper; + private SavingsAccountHelper savingsAccountHelper; + private SchedulerJobHelper scheduleJobHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + this.savingsProductHelper = new SavingsProductHelper(); + this.scheduleJobHelper = new SchedulerJobHelper(requestSpec); + } + + @Test + public void testSavingsDailyInterestPostingJob() { + // client activation, savings activation and 1st transaction date + final String startDate = "10 April 2022"; + final String jobName = "Post Interest For Savings"; + 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); + + /*** + * Runs Post interest posting job and verify the new account created with accounting configuration set as none + * is picked up by job + */ + this.scheduleJobHelper.executeAndAwaitJob(jobName); + Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); + ArrayList> transactions = (ArrayList>) transactionObj; + HashMap interestPostingTransaction = transactions.get(transactions.size() - 3); + for (Map.Entry entry : interestPostingTransaction.entrySet()) { + LOG.info("{} - {}", entry.getKey(), entry.getValue().toString()); + } + assertEquals("2.7405", interestPostingTransaction.get("amount").toString(), "Equality check for interest posted amount"); + assertEquals("[2022, 4, 12]", interestPostingTransaction.get("date").toString(), "Date check for Interest Posting transaction"); + } + + @Test + public void testSavingsDailyOverdraftInterestPostingJob() { + // client activation, savings activation and 1st transaction date + final String startDate = "10 April 2022"; + final String jobName = "Post Interest For Savings"; + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPostingOverdraft(clientID, startDate); + + 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.executeAndAwaitJob(jobName); + Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions"); + ArrayList> transactions = (ArrayList>) transactionObj; + HashMap interestPostingTransaction = transactions.get(transactions.size() - 2); + for (Map.Entry entry : interestPostingTransaction.entrySet()) { + LOG.info("{} - {}", entry.getKey(), entry.getValue().toString()); + } + 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"); + + } + + private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { + final Integer savingsProductID = createSavingsProductDailyPosting(); + Assertions.assertNotNull(savingsProductID); + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, + ACCOUNT_TYPE_INDIVIDUAL, startDate); + Assertions.assertNotNull(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + return savingsId; + } + + private Integer createSavingsAccountDailyPostingOverdraft(final Integer clientID, final String startDate) { + final Integer savingsProductID = createSavingsProductDailyPostingOverdraft(); + Assertions.assertNotNull(savingsProductID); + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, + ACCOUNT_TYPE_INDIVIDUAL, startDate); + Assertions.assertNotNull(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + return savingsId; + } + + private Integer createSavingsProductDailyPosting() { + final String savingsProductJSON = this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + private Integer createSavingsProductDailyPostingOverdraft() { + final String overDraftLimit = "10000.0"; + final String nominalAnnualInterestRateOverdraft = "10"; + final String savingsProductJSON = this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance() + .withOverDraftRate(overDraftLimit, nominalAnnualInterestRateOverdraft).build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + // Reset configuration fields + @AfterEach + public void tearDown() { + GlobalConfigurationHelper.resetAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec); + GlobalConfigurationHelper.verifyAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index 6ec624203..90127462e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -644,7 +644,7 @@ public class SavingsAccountHelper { } public Object getSavingsDetails(final Integer savingsID, final String returnAttribute) { - final String URL = SAVINGS_ACCOUNT_URL + "/" + savingsID + "?" + Utils.TENANT_IDENTIFIER; + final String URL = SAVINGS_ACCOUNT_URL + "/" + savingsID + "?associations=all&" + Utils.TENANT_IDENTIFIER; final Object response = Utils.performServerGet(requestSpec, responseSpec, URL, returnAttribute); return response; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java index 4b1b66b84..6ede76cbd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java @@ -96,6 +96,7 @@ public class SavingsProductHelper { private String daysToEscheat = null; private Boolean withgsimID = null; private Integer gsimID = null; + private String nominalAnnualInterestRateOverdraft = null; public String build() { final HashMap map = new HashMap<>(); @@ -134,6 +135,7 @@ public class SavingsProductHelper { map.put("lienAllowed", this.lienAllowed); map.put("maxAllowedLienLimit", this.maxAllowedLienLimit); map.put("withHoldTax", this.withHoldTax.toString()); + map.put("nominalAnnualInterestRateOverdraft", this.nominalAnnualInterestRateOverdraft); if (withHoldTax) { map.put("taxGroupId", taxGroupId); @@ -242,6 +244,13 @@ public class SavingsProductHelper { return this; } + public SavingsProductHelper withOverDraftRate(final String overdraftLimit, String nominalAnnualInterestRateOverdraft) { + this.allowOverdraft = "true"; + this.overdraftLimit = overdraftLimit; + this.nominalAnnualInterestRateOverdraft = nominalAnnualInterestRateOverdraft; + return this; + } + public SavingsProductHelper withWithHoldTax(final String taxGroupId) { if (taxGroupId != null) { this.withHoldTax = true;