diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 821f58e53..ee20f96d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -975,12 +975,12 @@ public class SavingsAccount extends AbstractPersistableCustom { boolean isTransactionsModified = false; for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { - if (transaction.isReversed()) { + if (transaction.isReversed() || transaction.isReversalTransaction()) { transaction.zeroBalanceFields(); } else { Money overdraftAmount = Money.zero(this.currency); Money transactionAmount = Money.zero(this.currency); - if (transaction.isCredit() || transaction.isAmountRelease()) { + if ((transaction.isCredit() || transaction.isAmountRelease())) { if (runningBalance.isLessThanZero()) { Money diffAmount = transaction.getAmount(this.currency).plus(runningBalance); if (diffAmount.isGreaterThanZero()) { @@ -998,7 +998,9 @@ public class SavingsAccount extends AbstractPersistableCustom { } runningBalance = runningBalance.plus(transactionAmount); - transaction.updateRunningBalance(runningBalance); + if (!transaction.getRunningBalance(transactionAmount.getCurrency()).isEqualTo(transactionAmount)) { + transaction.updateRunningBalance(runningBalance); + } if (overdraftAmount.isZero() && runningBalance.isLessThanZero()) { overdraftAmount = overdraftAmount.plus(runningBalance.getAmount().negate()); } @@ -1407,9 +1409,9 @@ public class SavingsAccount extends AbstractPersistableCustom { LocalDate lastSavingsDate = null; final BigDecimal withdrawalFee = null; for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { - if (transaction.isNotReversed() && transaction.isCredit()) { + if (transaction.isNotReversed() && transaction.isCredit() && !transaction.isReversalTransaction()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); - } else if (transaction.isNotReversed() && transaction.isDebit()) { + } else if (transaction.isNotReversed() && transaction.isDebit() && !transaction.isReversalTransaction()) { runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); } else { continue; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java index 796653c83..1498c92cc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java @@ -36,7 +36,8 @@ public final class SavingsAccountTransactionSummaryWrapper { public BigDecimal calculateTotalDeposits(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { - if (transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) { + if ((transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) + && !transaction.isReversalTransaction()) { total = total.plus(transaction.getAmount(currency)); } } @@ -56,7 +57,7 @@ public final class SavingsAccountTransactionSummaryWrapper { public BigDecimal calculateTotalWithdrawals(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { - if (transaction.isWithdrawal() && transaction.isNotReversed()) { + if (transaction.isWithdrawal() && transaction.isNotReversed() && !transaction.isReversalTransaction()) { total = total.plus(transaction.getAmount(currency)); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountBalanceCheckAfterReversalTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountBalanceCheckAfterReversalTest.java new file mode 100644 index 000000000..0e6c7e3a2 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountBalanceCheckAfterReversalTest.java @@ -0,0 +1,142 @@ +/** + * 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.HashMap; +import java.util.List; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.SchedulerJobHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SavingsAccountBalanceCheckAfterReversalTest { + + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + public static final String START_DATE = "10 April 2022"; + 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); + } + + @SuppressWarnings("unchecked") + @Test + public void testSavingsBalanceAfterWithdrawal() { + SavingsAccountHelper savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, + new ResponseSpecBuilder().build()); + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, START_DATE); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID); + Integer depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", START_DATE, + CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.reverseSavingsAccountTransaction(savingsId, depositTransactionId); + HashMap reversedDepositTransaction = this.savingsAccountHelper.getSavingsTransaction(savingsId, depositTransactionId); + Assertions.assertTrue((Boolean) reversedDepositTransaction.get("reversed")); + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + Float balance = Float.parseFloat("0.0"); + assertEquals(balance, summary.get("accountBalance"), "Verifying opening Balance is 0"); + List error = (List) savingsAccountHelperValidationError.withdrawalFromSavingsAccount(savingsId, "100", START_DATE, + CommonConstants.RESPONSE_ERROR); + assertEquals("error.msg.savingsaccount.transaction.insufficient.account.balance", + error.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); + } + + @Test + public void testSavingsBalanceWithOverDraftAfterWithdrawal() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, START_DATE); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPostingWithOverDraft(clientID); + Integer withdrawalTransactionId = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "1000", START_DATE, + CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.reverseSavingsAccountTransaction(savingsId, withdrawalTransactionId); + HashMap reversedWithdrawalTransaction = this.savingsAccountHelper.getSavingsTransaction(savingsId, withdrawalTransactionId); + Assertions.assertTrue((Boolean) reversedWithdrawalTransaction.get("reversed")); + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + Float balance = Float.parseFloat("0.0"); + assertEquals(balance, summary.get("accountBalance"), "Verifying Balance is 0"); + Integer withdrawalAfterReversalTransactionId = (Integer) savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "500", + START_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + balance = Float.parseFloat("-500.0"); + assertEquals(balance, summary.get("accountBalance"), "Verifying Balance is -500"); + } + + private Integer createSavingsAccountDailyPosting(final Integer clientID) { + final Integer savingsProductID = createSavingsProductDailyPosting(); + Assertions.assertNotNull(savingsProductID); + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, + ACCOUNT_TYPE_INDIVIDUAL, START_DATE); + Assertions.assertNotNull(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, START_DATE); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, START_DATE); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + return savingsId; + } + + private Integer createSavingsAccountDailyPostingWithOverDraft(final Integer clientID) { + final Integer savingsProductID = createSavingsProductDailyPostingWithOverDraft(); + Assertions.assertNotNull(savingsProductID); + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, + ACCOUNT_TYPE_INDIVIDUAL, START_DATE); + Assertions.assertNotNull(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, START_DATE); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, START_DATE); + 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 createSavingsProductDailyPostingWithOverDraft() { + final String savingsProductJSON = this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().withOverDraft("10000").build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + +}