diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java index 07c6f0023..9d560ad83 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java @@ -32,11 +32,13 @@ import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.security.domain.BasicPasswordEncodablePlatformUser; import org.apache.fineract.infrastructure.security.domain.PlatformUser; import org.apache.fineract.infrastructure.security.service.PlatformPasswordEncoder; @@ -268,27 +270,11 @@ public final class JsonCommand { } private boolean differenceExists(final TemporalAccessor baseValue, final TemporalAccessor workingCopyValue) { - boolean differenceExists = false; - - if (baseValue != null) { - differenceExists = !baseValue.equals(workingCopyValue); - } else { - differenceExists = workingCopyValue != null; - } - - return differenceExists; + return !Objects.equals(baseValue, workingCopyValue); } private boolean differenceExists(final String baseValue, final String workingCopyValue) { - boolean differenceExists = false; - - if (StringUtils.isNotBlank(baseValue)) { - differenceExists = !baseValue.equals(workingCopyValue); - } else { - differenceExists = StringUtils.isNotBlank(workingCopyValue); - } - - return differenceExists; + return !Objects.equals(baseValue, workingCopyValue); } private boolean differenceExists(final String[] baseValue, final String[] workingCopyValue) { @@ -298,47 +284,15 @@ public final class JsonCommand { } private boolean differenceExists(final Number baseValue, final Number workingCopyValue) { - boolean differenceExists = false; - - if (baseValue != null) { - if (workingCopyValue != null) { - differenceExists = !baseValue.equals(workingCopyValue); - } else { - differenceExists = true; - } - } else { - differenceExists = workingCopyValue != null; - } - - return differenceExists; + return !Objects.equals(baseValue, workingCopyValue); } private boolean differenceExists(final BigDecimal baseValue, final BigDecimal workingCopyValue) { - boolean differenceExists = false; - - if (baseValue != null) { - if (workingCopyValue != null) { - differenceExists = baseValue.compareTo(workingCopyValue) != 0; - } else { - differenceExists = true; - } - } else { - differenceExists = workingCopyValue != null; - } - - return differenceExists; + return !MathUtil.isEqualTo(baseValue, workingCopyValue); } private boolean differenceExists(final Boolean baseValue, final Boolean workingCopyValue) { - boolean differenceExists = false; - - if (baseValue != null) { - differenceExists = !baseValue.equals(workingCopyValue); - } else { - differenceExists = workingCopyValue != null; - } - - return differenceExists; + return !Objects.equals(baseValue, workingCopyValue); } public boolean parameterExists(final String parameterName) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationProperty.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationProperty.java index 6b884573a..a38401d24 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationProperty.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/GlobalConfigurationProperty.java @@ -64,10 +64,9 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom { private boolean isTrapDoor; public Map update(final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(7); - if (this.isTrapDoor == true) { + if (this.isTrapDoor) { throw new GlobalConfigurationPropertyCannotBeModfied(this.getId()); } @@ -109,7 +108,6 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom { } return actualChanges; - } public static GlobalConfigurationProperty newSurveyConfiguration(final String name) { @@ -120,6 +118,5 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom { return new GlobalConfigurationPropertyData().setName(getName()).setEnabled(isEnabled()).setValue(getValue()) .setDateValue(getDateValue()).setStringValue(getStringValue()).setId(this.getId()).setDescription(this.description) .setTrapDoor(this.isTrapDoor); - } } 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 5abf3857e..61a067910 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 @@ -83,6 +83,7 @@ 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.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; import org.apache.fineract.interoperation.domain.InteropIdentifier; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -510,6 +511,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + if (postingPeriods.isEmpty()) { + return; + } Money interestPostedToDate = Money.zero(this.currency); @@ -811,109 +815,103 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - // no openingBalance concept supported yet but probably will to allow - // for migrations. - Money openingAccountBalance = null; - + // no openingBalance concept supported yet but probably will to allow for migrations. // Check global configurations and 'pivot' date is null - if (backdatedTxnsAllowedTill) { - openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); - } else { - openingAccountBalance = Money.zero(this.currency); - } - - // update existing transactions so derived balance fields are - // correct. + Money openingAccountBalance = backdatedTxnsAllowedTill ? Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()) + : Money.zero(this.currency); + // update existing transactions so derived balance fields are correct. recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill, postReversals); - // 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(this.interestPostingPeriodType); - - final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType - .fromInt(this.interestCompoundingPeriodType); - - final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType - .fromInt(this.interestCalculationDaysInYearType); - List postedAsOnDates = null; - if (backdatedTxnsAllowedTill) { - postedAsOnDates = getManualPostingDatesWithPivotConfig(); - } else { - postedAsOnDates = getManualPostingDates(); - } - if (postInterestOnDate != null) { - postedAsOnDates.add(postInterestOnDate); - } - final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( - getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, - postedAsOnDates); - final List allPostingPeriods = new ArrayList<>(); + if (hasInterestCalculation() || hasOverdraftInterestCalculation()) { + // 1. default to calculate interest based on entire history OR + // 2. determine latest 'posting period' and find interest credited to that period - Money periodStartingBalance; - if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) { - LocalDate startInterestCalculationDate = this.startInterestCalculationDate; - SavingsAccountTransaction transaction = null; + // A generate list of EndOfDayBalances (not including interest postings) + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType + .fromInt(this.interestPostingPeriodType); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(this.interestCompoundingPeriodType); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(this.interestCalculationDaysInYearType); + List postedAsOnDates = null; if (backdatedTxnsAllowedTill) { - transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate); + postedAsOnDates = getManualPostingDatesWithPivotConfig(); } else { - transaction = findLastTransaction(startInterestCalculationDate); + postedAsOnDates = getManualPostingDates(); } + if (postInterestOnDate != null) { + postedAsOnDates.add(postInterestOnDate); + } + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( + getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, + postedAsOnDates); - if (transaction == null) { + Money periodStartingBalance; + if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) { + LocalDate startInterestCalculationDate = this.startInterestCalculationDate; + SavingsAccountTransaction transaction = null; + if (backdatedTxnsAllowedTill) { + transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate); + } else { + transaction = findLastTransaction(startInterestCalculationDate); + } + + if (transaction == null) { + periodStartingBalance = Money.zero(this.currency); + } else { + periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + } else { periodStartingBalance = Money.zero(this.currency); - } else { - periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); } - } else { - periodStartingBalance = Money.zero(this.currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(this.interestCalculationType); + final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate); + final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); + final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); + final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation); + + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + + boolean isUserPosting = false; + if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { + isUserPosting = true; + } + + PostingPeriod postingPeriod = null; + List orderedNonInterestPostingTransactions = null; + if (backdatedTxnsAllowedTill) { + orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig(); + } else { + orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions(); + } + + List savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList( + orderedNonInterestPostingTransactions); + + postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, + isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, + overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + + this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), + isTransferInterestToOtherAccount()); + + this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); } - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); - final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate); - final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc); - final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); - final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); - final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation); - - for (final LocalDateInterval periodInterval : postingPeriodIntervals) { - - boolean isUserPosting = false; - if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { - isUserPosting = true; - } - - PostingPeriod postingPeriod = null; - List orderedNonInterestPostingTransactions = null; - if (backdatedTxnsAllowedTill) { - orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig(); - } else { - orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions(); - } - - List savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList( - orderedNonInterestPostingTransactions); - - postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, - savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType, - interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, - isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, - overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); - - periodStartingBalance = postingPeriod.closingBalance(); - - allPostingPeriods.add(postingPeriod); - } - - this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), - isTransferInterestToOtherAccount()); - - this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); - if (backdatedTxnsAllowedTill) { this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, this.savingsAccountTransactions); @@ -933,6 +931,14 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); } + private boolean hasInterestCalculation() { + return !MathUtil.isEmpty(nominalAnnualInterestRate); + } + + private boolean hasOverdraftInterestCalculation() { + return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft); + } + protected List retreiveOrderedNonInterestPostingTransactions() { final List listOfTransactionsSorted = retrieveListOfTransactions(); @@ -982,7 +988,8 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, final boolean backdatedTxnsAllowedTill, boolean postReversals) { - Money runningBalance = openingAccountBalance.copy(); + Money runningBalance = openingAccountBalance; + boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation(); List accountTransactionsSorted = null; @@ -1017,15 +1024,14 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { } runningBalance = runningBalance.plus(transactionAmount); - if (!transaction.getRunningBalance(this.currency).isEqualTo(transactionAmount)) { - transaction.setRunningBalance(runningBalance); + transaction.setRunningBalance(runningBalance); + + if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { + overdraftAmount = runningBalance.negated(); } - if (overdraftAmount.isZero() && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { - overdraftAmount = overdraftAmount.plus(runningBalance.getAmount().negate()); - } - if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) { + if (!calculateInterest || transaction.getId() == null) { transaction.setOverdraftAmount(overdraftAmount); - } else if (overdraftAmount.isNotEqualTo(transaction.getOverdraftAmount(this.currency))) { + } else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency))) { SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction); if (transaction.isChargeTransaction()) { Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); @@ -1039,7 +1045,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { if (postReversals) { reversal = SavingsAccountTransaction.reversal(transaction); } - if (overdraftAmount.isGreaterThanZero()) { + if (MathUtil.isGreaterThanZero(overdraftAmount)) { accountTransaction.setOverdraftAmount(overdraftAmount); } accountTransaction.setRunningBalance(runningBalance); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 7f126fe38..ca47a7f62 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -63,6 +63,7 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; @@ -579,8 +580,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - if (savingsAccountData.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 || (savingsAccountData.isAllowOverdraft() - && savingsAccountData.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { + if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft() + && MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) { final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java index ab52ce6da..5f9be5a8a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java @@ -3162,56 +3162,6 @@ public class ClientSavingsIntegrationTest { error.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); } - @Test - public void testSavingsAccountDepositAfterNegativeHoldAmount() { - this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); - - final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); - ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); - - final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, "0", null, false, true, false); - Assertions.assertNotNull(savingsProductID); - - final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); - this.savingsAccountHelper.approveSavings(savingsId); - HashMap savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); - SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); - - float balance = 0F; - float transactionAmount = 100F; - Integer depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, - String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); - Assertions.assertNotNull(depositTransactionId); - balance = balance + transactionAmount; - HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); - assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after deposit"); - Integer withdrawalTransactionId = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, - String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); - Assertions.assertNotNull(withdrawalTransactionId); - balance = balance - transactionAmount; - summary = this.savingsAccountHelper.getSavingsSummary(savingsId); - assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after withdrawal"); - - float holdAmount = 50F; - Integer holdTransactionId = (Integer) this.savingsAccountHelper.holdAmountInSavingsAccount(savingsId, String.valueOf(holdAmount), - false, SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); - Assertions.assertNotNull(holdTransactionId); - balance = balance - holdAmount; - summary = this.savingsAccountHelper.getSavingsSummary(savingsId); - assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold amount"); - this.savingsAccountHelper.releaseAmount(savingsId, holdTransactionId); - balance = balance + holdAmount; - summary = this.savingsAccountHelper.getSavingsSummary(savingsId); - assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after release amount"); - - depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, String.valueOf(transactionAmount), - SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); - Assertions.assertNotNull(depositTransactionId); - balance = balance + transactionAmount; - summary = this.savingsAccountHelper.getSavingsSummary(savingsId); - assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold-release-deposit"); - } - private Integer createSavingsAccountDailyPostingOverdraft(final Integer clientID, final String startDate) { final Integer savingsProductID = createSavingsProductDailyPostingOverdraft(); Assertions.assertNotNull(savingsProductID); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountRecalculateBalanceTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountRecalculateBalanceTest.java new file mode 100644 index 000000000..4ae61978b --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountRecalculateBalanceTest.java @@ -0,0 +1,230 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertFalse; + +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.math.BigDecimal; +import java.util.HashMap; +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.PaymentTypeHelper; +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.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client Savings Integration Test for checking Savings Application. + */ +@SuppressWarnings({ "rawtypes" }) +@Order(2) +public class SavingsAccountRecalculateBalanceTest { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountRecalculateBalanceTest.class); + public static final String DEPOSIT_AMOUNT = "2000"; + public static final String WITHDRAW_AMOUNT = "1000"; + public static final String WITHDRAW_AMOUNT_ADJUSTED = "500"; + public static final String MINIMUM_OPENING_BALANCE = "1000.0"; + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + public static final String DATE_FORMAT = "dd MMMM yyyy"; + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private SavingsAccountHelper savingsAccountHelper; + private SavingsProductHelper savingsProductHelper; + private SchedulerJobHelper scheduleJobHelper; + private PaymentTypeHelper paymentTypeHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.requestSpec.header("Fineract-Platform-TenantId", "default"); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.paymentTypeHelper = new PaymentTypeHelper(); + } + + @Test + public void testSavingsAccountDepositAfterNegativeHoldAmount() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, "0", null, false, true, false, null); + Assertions.assertNotNull(savingsProductID); + + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + this.savingsAccountHelper.approveSavings(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + float balance = 0F; + float transactionAmount = 100F; + Integer depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, + String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(depositTransactionId); + balance = balance + transactionAmount; + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after deposit"); + Integer withdrawalTransactionId = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, + String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(withdrawalTransactionId); + balance = balance - transactionAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after withdrawal"); + + float holdAmount = 50F; + Integer holdTransactionId = (Integer) this.savingsAccountHelper.holdAmountInSavingsAccount(savingsId, String.valueOf(holdAmount), + false, SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(holdTransactionId); + balance = balance - holdAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold amount"); + this.savingsAccountHelper.releaseAmount(savingsId, holdTransactionId); + balance = balance + holdAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after release amount"); + + depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, String.valueOf(transactionAmount), + SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(depositTransactionId); + balance = balance + transactionAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold-release-deposit"); + } + + @Test + public void testSavingsAccountDepositAfterNegativeHoldAmountNoInterest() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, "0", null, false, true, false, + BigDecimal.ZERO); + Assertions.assertNotNull(savingsProductID); + + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + this.savingsAccountHelper.approveSavings(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + float balance = 0F; + float transactionAmount = 100F; + Integer depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, + String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(depositTransactionId); + balance = balance + transactionAmount; + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after deposit"); + HashMap depositTransaction = savingsAccountHelper.getTransactionDetails(savingsId, depositTransactionId); + assertEquals(balance, depositTransaction.get("runningBalance"), "Verifying Running Balance of deposit"); + Integer withdrawalTransactionId = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, + String.valueOf(transactionAmount), SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(withdrawalTransactionId); + balance = balance - transactionAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after withdrawal"); + HashMap withdrawalTransaction = savingsAccountHelper.getTransactionDetails(savingsId, withdrawalTransactionId); + assertEquals(balance, withdrawalTransaction.get("runningBalance"), "Verifying Running Balance of withdraw"); + + float holdAmount = 50F; + Integer holdTransactionId = (Integer) this.savingsAccountHelper.holdAmountInSavingsAccount(savingsId, String.valueOf(holdAmount), + false, SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(holdTransactionId); + balance = balance - holdAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold amount"); + Integer releaseTransactionId = this.savingsAccountHelper.releaseAmount(savingsId, holdTransactionId); + Assertions.assertNotNull(releaseTransactionId); + balance = balance + holdAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after release amount"); + + depositTransactionId = (Integer) this.savingsAccountHelper.depositToSavingsAccount(savingsId, String.valueOf(transactionAmount), + SavingsAccountHelper.TRANSACTION_DATE, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(depositTransactionId); + balance = balance + transactionAmount; + summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(balance, summary.get("availableBalance"), "Verifying Balance after hold-release-deposit"); + depositTransaction = savingsAccountHelper.getTransactionDetails(savingsId, depositTransactionId); + // this is a backdated transaction and so listed before the release transaction + assertEquals(balance - holdAmount, depositTransaction.get("runningBalance"), + "Verifying Running Balance of deposit negative balance"); + + HashMap releaseTransaction = savingsAccountHelper.getTransactionDetails(savingsId, releaseTransactionId); + assertFalse((Boolean) releaseTransaction.get("reversed"), "Verifying release transaction with overdraft is not reversed"); + assertEquals(balance, releaseTransaction.get("runningBalance"), "Verifying Running Balance"); + } + + // LienAtProductLevel + private Integer createSavingsProduct(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String minOpenningBalance, String minBalanceForInterestCalculation, final boolean enforceMinRequiredBalance, + final boolean allowOverDraft, final boolean lienAllowed, BigDecimal interestRate) { + + LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT WITH LIEN---------------------------------------"); + SavingsProductHelper savingsProductHelper = new SavingsProductHelper(); + if (lienAllowed) { + final String maxAllowedLienLimit = "2000.0"; + savingsProductHelper.withLienAllowed(maxAllowedLienLimit); + } + if (enforceMinRequiredBalance) { + final String minRequiredBalance = "100.0"; + savingsProductHelper.withMinRequiredBalance(minRequiredBalance); + savingsProductHelper.withEnforceMinRequiredBalance("true"); + } + if (allowOverDraft) { + final String overDraftLimit = "500.0"; + savingsProductHelper.withOverDraft(overDraftLimit); + } + if (interestRate != null) { + savingsProductHelper.withNominalAnnualInterestRate(interestRate); + } + final String savingsProductJSON = savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance() + .withMinBalanceForInterestCalculation(minBalanceForInterestCalculation).withMinimumOpenningBalance(minOpenningBalance) + .build(); + + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + @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 0c14c2672..a0b7d9cff 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 @@ -122,30 +122,35 @@ public class SavingsAccountHelper extends IntegrationTest { return sdf.format(calendar.getTime()); } - public Integer applyForSavingsApplication(final Integer id, final Integer savingsProductID, final String accountType) { - return applyForSavingsApplicationOnDate(id, savingsProductID, accountType, CREATED_DATE); + public Integer applyForSavingsApplication(final Integer clientOrGroupId, final Integer savingsProductID, final String accountType) { + return applyForSavingsApplicationOnDate(clientOrGroupId, savingsProductID, accountType, CREATED_DATE); } - public Integer applyForSavingsApplicationOnDate(final Integer id, final Integer savingsProductID, final String accountType, + public Integer applyForSavingsApplicationOnDate(final Integer clientOrGroupId, final Integer savingsProductID, final String accountType, final String submittedOnDate) { - return applyForSavingsApplicationOnDate(id, savingsProductID, accountType, null, false, submittedOnDate); + return applyForSavingsApplicationOnDate(clientOrGroupId, savingsProductID, accountType, null, false, submittedOnDate); } - public Integer applyForSavingsApplicationWithExternalId(final Integer id, final Integer savingsProductID, final String accountType, - String externalId, boolean withdrawalFeeForTransfers) { - return applyForSavingsApplicationOnDate(id, savingsProductID, accountType, externalId, withdrawalFeeForTransfers, CREATED_DATE); + public Integer applyForSavingsApplicationWithExternalId(final Integer clientOrGroupId, final Integer savingsProductID, + final String accountType, String externalId, boolean withdrawalFeeForTransfers) { + return applyForSavingsApplicationOnDate(clientOrGroupId, savingsProductID, accountType, externalId, withdrawalFeeForTransfers, + CREATED_DATE); } - public Integer applyForSavingsApplicationOnDate(final Integer id, final Integer savingsProductID, final String accountType, + public Integer applyForSavingsApplicationOnDate(final Integer clientOrGroupId, final Integer savingsProductID, final String accountType, String externalId, boolean withdrawalFeeForTransfers, final String submittedOnDate) { - LOG.info("--------------------------------APPLYING FOR SAVINGS APPLICATION--------------------------------"); final String savingsApplicationJSON = new SavingsApplicationTestBuilder() // .withExternalId(externalId) // .withWithdrawalFeeForTransfers(withdrawalFeeForTransfers) // .withSubmittedOnDate(submittedOnDate) // - .build(id.toString(), savingsProductID.toString(), accountType); + .build(clientOrGroupId.toString(), savingsProductID.toString(), accountType); + return applyForSavingsApplicationOnDate(savingsApplicationJSON); + } + + public Integer applyForSavingsApplicationOnDate(String savingsApplicationJson) { + LOG.info("--------------------------------APPLYING FOR SAVINGS APPLICATION--------------------------------"); return Utils.performServerPost(this.requestSpec, this.responseSpec, SAVINGS_ACCOUNT_URL + "?" + Utils.TENANT_IDENTIFIER, - savingsApplicationJSON, "savingsId"); + savingsApplicationJson, "savingsId"); } public Integer applyForSavingsApplicationWithDatatables(final Integer id, final Integer savingsProductID, final String accountType, @@ -923,4 +928,11 @@ public class SavingsAccountHelper extends IntegrationTest { updateGsimJSON(clientID.toString(), groupID.toString(), productID.toString()), ""); } + public HashMap getTransactionDetails(Integer savingsId, Integer transactionId) { + LOG.info("--------------------------------- GET savings transaction details -------------------------------"); + final String url = "/fineract-provider/api/v1/savingsaccounts/" + savingsId + "/transactions/" + transactionId + "?" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, responseSpec, url, ""); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsApplicationTestBuilder.java index 6666da98e..3faf31929 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsApplicationTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsApplicationTestBuilder.java @@ -43,14 +43,14 @@ public class SavingsApplicationTestBuilder { private List> clientArray = null; private List> savingsArray = null; - public String build(final String id, final String savingsProductId, final String accountType) { + public String build(final String clientOrGroupId, final String savingsProductId, final String accountType) { final HashMap map = new HashMap<>(); map.put("dateFormat", "dd MMMM yyyy"); if (accountType.equals("GROUP")) { - map.put("groupId", id); + map.put("groupId", clientOrGroupId); } else { - map.put("clientId", id); + map.put("clientId", clientOrGroupId); } map.put("productId", savingsProductId); map.put("locale", LOCALE);