FINERACT-2011: Savings Account - transaction order is incorrectly mixed after certain activities

This commit is contained in:
jmarta
2023-11-20 11:50:31 +01:00
committed by Adam Saghy
parent d0341e69cb
commit 4f2400ca3e
8 changed files with 371 additions and 221 deletions
@@ -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) {
@@ -64,10 +64,9 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom {
private boolean isTrapDoor;
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> 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);
}
}
@@ -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<PostingPeriod> 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<LocalDate> postedAsOnDates = null;
if (backdatedTxnsAllowedTill) {
postedAsOnDates = getManualPostingDatesWithPivotConfig();
} else {
postedAsOnDates = getManualPostingDates();
}
if (postInterestOnDate != null) {
postedAsOnDates.add(postInterestOnDate);
}
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth,
postedAsOnDates);
final List<PostingPeriod> 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<LocalDate> postedAsOnDates = null;
if (backdatedTxnsAllowedTill) {
transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate);
postedAsOnDates = getManualPostingDatesWithPivotConfig();
} else {
transaction = findLastTransaction(startInterestCalculationDate);
postedAsOnDates = getManualPostingDates();
}
if (postInterestOnDate != null) {
postedAsOnDates.add(postInterestOnDate);
}
final List<LocalDateInterval> 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<Long> 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<SavingsAccountTransaction> orderedNonInterestPostingTransactions = null;
if (backdatedTxnsAllowedTill) {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig();
} else {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions();
}
List<SavingsAccountTransactionDetailsForPostingPeriod> 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<Long> 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<SavingsAccountTransaction> orderedNonInterestPostingTransactions = null;
if (backdatedTxnsAllowedTill) {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig();
} else {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions();
}
List<SavingsAccountTransactionDetailsForPostingPeriod> 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<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() {
final List<SavingsAccountTransaction> 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<SavingsAccountTransaction> 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<SavingsAccountChargePaidBy> 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);
@@ -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<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds);
@@ -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);
@@ -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);
}
}
@@ -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, "");
}
}
@@ -43,14 +43,14 @@ public class SavingsApplicationTestBuilder {
private List<Map<String, Object>> clientArray = null;
private List<Map<String, Object>> 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<String, Object> 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);