This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 5d011077e FINERACT-1912 Refactored Implementation of Savings Account
Transaction Search
5d011077e is described below
commit 5d011077e74833af554c50e218c05f815255c2b5
Author: Mkkor <[email protected]>
AuthorDate: Mon May 22 20:13:50 2023 +0530
FINERACT-1912 Refactored Implementation of Savings Account Transaction
Search
FINERACT-1912 Refactored Implementation of Savings Account Transaction
Search
FINERACT-1912 Refactored Implementation of Savings Account Transaction
Search
FINERACT-1912 Savings Account Transaction Advanced Search Support
FINERACT-1912 Savings Account Transaction Advanced Search Support
Support Pagination, Sorting and Filtering By transaction Type, Transaction
Date and Amount
Supported Filtering for Amount and Date,
GTE, LTE, GT, LT
Supported Multiple Transaction Types Filtering
---
.../core/data/DataValidatorBuilder.java | 17 +
.../infrastructure/core/data/RangeOperator.java | 31 ++
.../account/domain/AccountTransferTransaction.java | 2 +
.../api/SavingsAccountTransactionsApiResource.java | 19 +
...vingsAccountTransactionsApiResourceSwagger.java | 161 ++++++++
.../SavingsAccountTransactionSearchValidator.java | 95 +++++
.../data/SavingsTransactionSearchResult.java | 147 ++++++++
.../SavingsAccountTransactionRepository.java | 5 +-
.../search/BaseQueryParametersMapResult.java | 29 ++
.../domain/search/SavingsTransactionSearch.java | 51 +++
.../search/SavingsTransactionSearchParameters.java | 39 ++
.../SavingsTransactionsSearchRepository.java | 27 ++
.../SavingsTransactionsSearchRepositoryImpl.java | 186 +++++++++
.../SavingsAccountTransactionSearchService.java | 30 ++
...avingsAccountTransactionsSearchServiceImpl.java | 76 ++++
...gsAccountTransactionsSearchIntegrationTest.java | 414 +++++++++++++++++++++
.../common/savings/SavingsAccountHelper.java | 13 +
17 files changed, 1340 insertions(+), 2 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
index 39b367c73..b1313d24c 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
@@ -275,6 +275,23 @@ public class DataValidatorBuilder {
return this;
}
+ public DataValidatorBuilder notExceedingListLengthOf(final Integer
maxLength) {
+ if (this.value == null && this.ignoreNullValue) {
+ return this;
+ }
+
+ if (this.value instanceof List && ((List<?>) this.value).size() >
maxLength) {
+ final StringBuilder validationErrorCode = new
StringBuilder("validation.msg.").append(this.resource).append(".")
+
.append(this.parameter).append(".exceeds.max.length.allowed");
+ final StringBuilder defaultEnglishMessage = new StringBuilder("The
parameter `").append(this.parameter)
+ .append("` exceeds allowed max length of
").append(maxLength).append(".");
+ final ApiParameterError error =
ApiParameterError.parameterError(validationErrorCode.toString(),
+ defaultEnglishMessage.toString(), this.parameter);
+ this.dataValidationErrors.add(error);
+ }
+ return this;
+ }
+
public DataValidatorBuilder inMinMaxRange(final Integer min, final Integer
max) {
if (this.value == null && this.ignoreNullValue) {
return this;
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java
new file mode 100644
index 000000000..67977e4b2
--- /dev/null
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java
@@ -0,0 +1,31 @@
+/**
+ * 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.infrastructure.core.data;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum RangeOperator {
+
+ GTE(">="), LTE("<="), GT(">"), LT("<");
+
+ private final String symbol;
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java
index 7857dd47f..cb6608741 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java
@@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
+import lombok.Getter;
import
org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -34,6 +35,7 @@ import
org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
@Entity
@Table(name = "m_account_transfer_transaction")
+@Getter
public class AccountTransferTransaction extends AbstractPersistableCustom {
@ManyToOne
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
index 4ca7650c3..bbbc66218 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.savings.api;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -48,13 +49,17 @@ import
org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx
import
org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import
org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import
org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.core.service.Page;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
import
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import
org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
import
org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
+import
org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
@@ -71,6 +76,7 @@ public class SavingsAccountTransactionsApiResource {
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountReadPlatformService
savingsAccountReadPlatformService;
private final PaymentTypeReadPlatformService
paymentTypeReadPlatformService;
+ private final SavingsAccountTransactionsSearchServiceImpl
transactionsSearchServiceImpl;
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) &&
commandParam.trim().equalsIgnoreCase(commandValue);
@@ -118,6 +124,19 @@ public class SavingsAccountTransactionsApiResource {
SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS);
}
+ @POST
+ @Path("search")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Search Savings Account Transactions")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "OK", content =
@Content(schema = @Schema(implementation =
SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class)))
})
+ public String searchTransactions(@PathParam("savingsId")
@Parameter(description = "savingsId") final Long savingsId,
+ @Parameter PagedRequest<SavingsTransactionSearch> searchRequest) {
+ Page<SavingsAccountTransactionData> transactionsData =
transactionsSearchServiceImpl.searchTransactions(savingsId, searchRequest);
+ return toApiJsonSerializer.serialize(transactionsData);
+ }
+
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java
index 3bd433b16..d1c7e7311 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java
@@ -20,11 +20,172 @@ package org.apache.fineract.portfolio.savings.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.Set;
final class SavingsAccountTransactionsApiResourceSwagger {
private SavingsAccountTransactionsApiResourceSwagger() {}
+ @Schema(description = "SavingsAccountTransactionsSearchResponse")
+ public static final class SavingsAccountTransactionsSearchResponse {
+
+ private SavingsAccountTransactionsSearchResponse() {}
+
+ static final class GetSavingsAccountTransactionsPageItem {
+
+ private GetSavingsAccountTransactionsPageItem() {}
+
+ static final class GetTranscationEnumData {
+
+ private GetTranscationEnumData() {}
+
+ @Schema(example = "1")
+ public Long id;
+ @Schema(example = "savingsAccountTransactionType.deposit")
+ public String code;
+ @Schema(example = "Deposit")
+ public String value;
+
+ @Schema(example = "true")
+ public boolean deposit;
+ @Schema(example = "false")
+ public boolean dividendPayout;
+ @Schema(example = "false")
+ public boolean withdrawal;
+ @Schema(example = "false")
+ public boolean interestPosting;
+ @Schema(example = "false")
+ public boolean feeDeduction;
+ @Schema(example = "false")
+ public boolean initiateTransfer;
+ @Schema(example = "false")
+ public boolean approveTransfer;
+ @Schema(example = "false")
+ public boolean withdrawTransfer;
+ @Schema(example = "false")
+ public boolean rejectTransfer;
+ @Schema(example = "false")
+ public boolean overdraftInterest;
+ @Schema(example = "false")
+ public boolean writtenoff;
+ @Schema(example = "true")
+ public boolean overdraftFee;
+ @Schema(example = "false")
+ public boolean withholdTax;
+ @Schema(example = "false")
+ public boolean escheat;
+ @Schema(example = "false")
+ public boolean amountHold;
+ @Schema(example = "false")
+ public boolean amountRelease;
+ }
+
+ static final class GetTransactionsCurrency {
+
+ private GetTransactionsCurrency() {}
+
+ @Schema(example = "USD")
+ public String code;
+ @Schema(example = "US Dollar")
+ public String name;
+ @Schema(example = "2")
+ public Integer decimalPlaces;
+ @Schema(example = "0")
+ public Integer isMultiplesOf;
+ @Schema(example = "$")
+ public String displaySymbol;
+ @Schema(example = "currency.USD")
+ public String nameCode;
+ @Schema(example = "US Dollar ($)")
+ public String displayLabel;
+ }
+
+ static final class GetTransactionsPaymentDetailData {
+
+ private GetTransactionsPaymentDetailData() {}
+
+ static final class GetPaymentTypeData {
+
+ private GetPaymentTypeData() {}
+
+ @Schema(example = "1")
+ public Long id;
+ @Schema(example = "Money Transfer")
+ public String name;
+ @Schema(example = "false")
+ public Boolean isSystemDefined;
+
+ }
+
+ @Schema(example = "1")
+ public Long id;
+ public GetPaymentTypeData paymentType;
+ @Schema(example = "acc123")
+ public String accountNumber;
+ @Schema(example = "che123")
+ public String checkNumber;
+ @Schema(example = "rou123")
+ public String routingCode;
+ @Schema(example = "rec123")
+ public String receiptNumber;
+ @Schema(example = "ban123")
+ public String bankNumber;
+ }
+
+ static final class GetSavingsAccountChargesPaidByData {
+
+ private GetSavingsAccountChargesPaidByData() {}
+
+ @Schema(example = "1")
+ public Long chargeId;
+ @Schema(example = "0")
+ public BigDecimal amount;
+ }
+
+ @Schema(example = "1")
+ public Long id;
+ public GetTranscationEnumData transactionType;
+ @Schema(example = "1")
+ public Long accountId;
+ @Schema(example = "000000001")
+ public String accountNo;
+ @Schema(example = "[2023, 05, 01]")
+ public LocalDate date;
+ public GetTransactionsCurrency currency;
+ public GetTransactionsPaymentDetailData paymentDetailData;
+ @Schema(example = "500")
+ public BigDecimal amount;
+ @Schema(example = "500")
+ public BigDecimal runningBalance;
+ @Schema(example = "false")
+ public boolean reversed;
+ @Schema(example = "[2023, 05, 01]")
+ public LocalDate submittedOnDate;
+ @Schema(example = "false")
+ public boolean interestedPostedAsOn;
+ @Schema(example = "mifos")
+ public String submittedByUsername;
+ @Schema(example = "false")
+ public boolean isManualTransaction;
+ @Schema(example = "false")
+ public Boolean isReversal;
+ @Schema(example = "0")
+ public Long originalTransactionId;
+ @Schema(example = "false")
+ public Boolean lienTransaction;
+ @Schema(example = "0")
+ public Long releaseTransactionId;
+ public Set<GetSavingsAccountChargesPaidByData> chargesPaidByData =
new HashSet<>();
+
+ }
+
+ @Schema(example = "2")
+ public Long totalFilteredRecords;
+ public Set<GetSavingsAccountTransactionsPageItem> pageItems;
+ }
+
@Schema(description = "PostSavingsAccountTransactionsRequest")
public static final class PostSavingsAccountTransactionsRequest {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java
new file mode 100644
index 000000000..39a17334a
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.data;
+
+import static
org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName;
+import static
org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.data.RangeOperator;
+import
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.portfolio.savings.SavingsApiConstants;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SavingsAccountTransactionSearchValidator {
+
+ public void validateSearchFilters(SavingsTransactionSearch.Filters
searchFilters) {
+ final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+ final DataValidatorBuilder baseDataValidator = new
DataValidatorBuilder(dataValidationErrors)
+
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME);
+ if (searchFilters != null) {
+
+ List<RangeFilter<LocalDate>> dateFilters =
searchFilters.getTransactionDate();
+ validateRangeFilters(baseDataValidator, dateFilters,
transactionDateParamName);
+
+ List<RangeFilter<BigDecimal>> amountFilters =
searchFilters.getTransactionAmount();
+ validateRangeFilters(baseDataValidator, amountFilters,
transactionAmountParamName);
+ }
+ throwExceptionIfValidationWarningsExist(dataValidationErrors);
+ }
+
+ private <T> void validateRangeFilters(DataValidatorBuilder
baseDataValidator, List<RangeFilter<T>> rangeFilters, String paramName) {
+ if (rangeFilters == null) {
+ return;
+ }
+
+ if (rangeFilters.size() > 2) {
+
baseDataValidator.parameter(paramName).value(rangeFilters).notExceedingListLengthOf(2);
+ }
+
+ if (!rangeFilters.isEmpty()) {
+ RangeFilter<T> firstFilter = rangeFilters.get(0);
+ RangeOperator firstOperator = firstFilter.getOperator();
+
+ if (rangeFilters.size() == 2) {
+ RangeFilter<T> secondFilter = rangeFilters.get(1);
+ RangeOperator secondOperator = secondFilter.getOperator();
+
+ if (((firstOperator == RangeOperator.GT || firstOperator ==
RangeOperator.GTE)
+ && !(secondOperator == RangeOperator.LT ||
secondOperator == RangeOperator.LTE))
+ || ((firstOperator == RangeOperator.LT ||
firstOperator == RangeOperator.LTE)
+ && !(secondOperator == RangeOperator.GT ||
secondOperator == RangeOperator.GTE))) {
+
baseDataValidator.parameter(paramName).failWithCode("invalid.range",
firstOperator, secondOperator);
+ }
+ }
+ }
+
+ for (RangeFilter<T> filter : rangeFilters) {
+ T value = filter.getValue();
+ if (value instanceof BigDecimal) {
+
baseDataValidator.parameter(paramName).value(value).zeroOrPositiveAmount();
+ }
+ }
+ }
+
+ private void throwExceptionIfValidationWarningsExist(final
List<ApiParameterError> dataValidationErrors) {
+ if (!dataValidationErrors.isEmpty()) {
+ throw new
PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.",
+ dataValidationErrors);
+ }
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java
new file mode 100644
index 000000000..4ab2ad47c
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.apache.fineract.organisation.monetary.data.CurrencyData;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
+import org.apache.fineract.portfolio.account.data.AccountTransferData;
+import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
+import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
+import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
+import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
+import org.apache.fineract.useradministration.domain.AppUser;
+
+@Getter
+@AllArgsConstructor
+public class SavingsTransactionSearchResult {
+
+ private Long transactionId;
+ private Integer transactionType;
+ private LocalDate transactionDate;
+ private BigDecimal transactionAmount;
+ private Long releaseIdOfHoldAmountTransaction;
+ private String reasonForBlock;
+ private LocalDateTime createdDate;
+ private AppUser appUser;
+ private String note;
+ private BigDecimal runningBalance;
+ private boolean reversed;
+ private boolean reversalTransaction;
+ private Long originalTxnId;
+ private Boolean lienTransaction;
+ private boolean isManualTransaction;
+ private AccountTransferTransaction fromSavingsTransaction;
+ private AccountTransferTransaction toSavingsTransaction;
+ private SavingsAccount savingsAccount;
+ private PaymentDetail paymentDetail;
+ private ApplicationCurrency currency;
+
+ public static final SavingsAccountTransactionData
toSavingsAccountTransactionData(SavingsTransactionSearchResult dto) {
+ final Long id = dto.getTransactionId();
+ final int transactionTypeInt = dto.getTransactionType();
+
+ final SavingsAccountTransactionEnumData transactionType =
SavingsEnumerations.transactionType(transactionTypeInt);
+
+ final LocalDate date = dto.getTransactionDate();
+ final LocalDate submittedOnDate =
Optional.ofNullable(dto.getCreatedDate()).map(LocalDateTime::toLocalDate).orElse(null);
+ final BigDecimal amount =
Optional.ofNullable(dto.getTransactionAmount()).orElse(BigDecimal.ZERO);
+ final Long releaseTransactionId =
Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(null);
+ final String reasonForBlock =
Optional.ofNullable(dto.getReasonForBlock()).orElse(null);
+ final BigDecimal outstandingChargeAmount = null;
+ final BigDecimal runningBalance =
Optional.ofNullable(dto.getRunningBalance()).orElse(BigDecimal.ZERO);
+ final boolean reversed = dto.isReversed();
+ final boolean isReversal = dto.isReversalTransaction();
+ final Long originalTransactionId =
Optional.ofNullable(dto.getOriginalTxnId()).orElse(null);
+ final Boolean lienTransaction = dto.getLienTransaction();
+
+ final Long savingsId =
Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount ->
savingsAccount.getId()).orElse(null);
+ final String accountNo =
Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount ->
savingsAccount.getAccountNumber())
+ .orElse(null);
+ final boolean postInterestAsOn = dto.isManualTransaction();
+
+ PaymentDetailData paymentDetailData = null;
+ if (Objects.nonNull(transactionType) &&
transactionType.isDepositOrWithdrawal()) {
+ final PaymentDetail paymentDetail = dto.getPaymentDetail();
+ if (Objects.nonNull(paymentDetail)) {
+ final Long paymentTypeId =
Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType ->
paymentType.getId())
+ .orElse(null);
+ if (Objects.nonNull(paymentTypeId)) {
+ final String typeName =
Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType ->
paymentType.getName())
+ .orElse(null);
+ final PaymentTypeData paymentType =
PaymentTypeData.instance(paymentTypeId, typeName);
+ final String accountNumber =
paymentDetail.getAccountNumber();
+ final String checkNumber = paymentDetail.getCheckNumber();
+ final String routingCode = paymentDetail.getRoutingCode();
+ final String receiptNumber =
paymentDetail.getReceiptNumber();
+ final String bankNumber = paymentDetail.getBankNumber();
+ paymentDetailData = new PaymentDetailData(id, paymentType,
accountNumber, checkNumber, routingCode, receiptNumber,
+ bankNumber);
+ }
+ }
+ }
+
+ final String currencyCode =
dto.getSavingsAccount().getCurrency().getCode();
+ final String currencyName = dto.getCurrency().getName();
+ final String currencyNameCode = dto.getCurrency().getNameCode();
+ final String currencyDisplaySymbol =
dto.getCurrency().getDisplaySymbol();
+ final Integer currencyDigits =
dto.getSavingsAccount().getCurrency().getDigitsAfterDecimal();
+ final Integer inMultiplesOf =
dto.getSavingsAccount().getCurrency().getCurrencyInMultiplesOf();
+ final CurrencyData currency = new CurrencyData(currencyCode,
currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol,
+ currencyNameCode);
+
+ AccountTransferData transfer = null;
+ AccountTransferTransaction transferFrom =
dto.getFromSavingsTransaction();
+ AccountTransferTransaction transferTo = dto.getToSavingsTransaction();
+ if (Objects.nonNull(transferFrom)) {
+ final Long fromTransferId = transferFrom.getId();
+ final LocalDate fromTransferDate = transferFrom.getDate();
+ final BigDecimal fromTransferAmount =
Optional.ofNullable(transferFrom.getAmount()).orElse(BigDecimal.ZERO);
+ final boolean fromTransferReversed = transferFrom.isReversed();
+ final String fromTransferDescription =
transferFrom.getDescription();
+
+ transfer =
AccountTransferData.transferBasicDetails(fromTransferId, currency,
fromTransferAmount, fromTransferDate,
+ fromTransferDescription, fromTransferReversed);
+ } else if (Objects.nonNull(transferTo)) {
+ final Long toTransferId = transferTo.getId();
+ final LocalDate toTransferDate = transferTo.getDate();
+ final BigDecimal toTransferAmount =
Optional.ofNullable(transferTo.getAmount()).orElse(BigDecimal.ZERO);
+ final boolean toTransferReversed = transferTo.isReversed();
+ final String toTransferDescription = transferTo.getDescription();
+
+ transfer = AccountTransferData.transferBasicDetails(toTransferId,
currency, toTransferAmount, toTransferDate,
+ toTransferDescription, toTransferReversed);
+ }
+ final String submittedByUsername =
Optional.ofNullable(dto.getAppUser()).map(user ->
user.getUsername()).orElse(null);
+ final String note = Optional.ofNullable(dto.getNote()).orElse(null);
+ return SavingsAccountTransactionData.create(id, transactionType,
paymentDetailData, savingsId, accountNo, date, currency, amount,
+ outstandingChargeAmount, runningBalance, reversed, transfer,
submittedOnDate, postInterestAsOn, submittedByUsername, note,
+ isReversal, originalTransactionId, lienTransaction,
releaseTransactionId, reasonForBlock);
+
+ }
+
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java
index 2eba64bb2..ae0709658 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.LockModeType;
import java.time.LocalDate;
import java.util.List;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionsSearchRepository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -28,8 +29,8 @@ import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-public interface SavingsAccountTransactionRepository
- extends JpaRepository<SavingsAccountTransaction, Long>,
JpaSpecificationExecutor<SavingsAccountTransaction> {
+public interface SavingsAccountTransactionRepository extends
JpaRepository<SavingsAccountTransaction, Long>,
+ JpaSpecificationExecutor<SavingsAccountTransaction>,
SavingsTransactionsSearchRepository {
@Query("select sat from SavingsAccountTransaction sat where sat.id =
:transactionId and sat.savingsAccount.id = :savingsId")
SavingsAccountTransaction
findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java
new file mode 100644
index 000000000..4522f0169
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.domain.search;
+
+import java.util.Map;
+import lombok.Data;
+
+@Data
+public class BaseQueryParametersMapResult {
+
+ private final String baseQueryString;
+ private final Map<String, Object> parametersMap;
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java
new file mode 100644
index 000000000..ef09050bb
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.domain.search;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Data;
+import org.apache.fineract.infrastructure.core.data.RangeOperator;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+@Data
+public class SavingsTransactionSearch {
+
+ private Filters filters;
+
+ @Data
+ public static class Filters {
+
+ private List<RangeFilter<LocalDate>> transactionDate;
+
+ private List<RangeFilter<BigDecimal>> transactionAmount;
+
+ private List<SavingsAccountTransactionType> transactionType;
+ }
+
+ @Data
+ public static class RangeFilter<T> {
+
+ private RangeOperator operator;
+
+ private T value;
+ }
+
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java
new file mode 100644
index 000000000..4ec2f4dd8
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.domain.search;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.apache.fineract.portfolio.savings.DepositAccountType;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters;
+import org.springframework.data.domain.Pageable;
+
+@Getter
+@Builder
+public class SavingsTransactionSearchParameters {
+
+ private Long savingsId;
+
+ private DepositAccountType depositAccountType;
+
+ private Filters filters;
+
+ private Pageable pageable;
+
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java
new file mode 100644
index 000000000..8a881154e
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.domain.search;
+
+import
org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult;
+import org.springframework.data.domain.Page;
+
+public interface SavingsTransactionsSearchRepository {
+
+ Page<SavingsTransactionSearchResult>
searchTransactions(SavingsTransactionSearchParameters searchParameters);
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java
new file mode 100644
index 000000000..2a92fea05
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java
@@ -0,0 +1,186 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.domain.search;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.portfolio.savings.DepositAccountType;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+import
org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class SavingsTransactionsSearchRepositoryImpl implements
SavingsTransactionsSearchRepository {
+
+ private static final String AMOUNT_FIELD_NAME = "amount";
+ private static final String ID_FIELD_NAME = "id";
+ private static final String CREATED_DATE_FIELD_NAME = "createdDate";
+ private static final String TRANSACTION_DATE_FIELD_NAME = "dateOf";
+ private final EntityManager entityManager;
+
+ @Override
+ public Page<SavingsTransactionSearchResult>
searchTransactions(SavingsTransactionSearchParameters searchParameters) {
+ // Build base query with filters but without the selection
+ BaseQueryParametersMapResult baseQueryParameterMapResult =
buildBaseQueryWithFilters(searchParameters.getSavingsId(),
+ searchParameters.getDepositAccountType(),
searchParameters.getFilters());
+
+ // Attach the selection
+ String jpqlQuery =
attachSelection(baseQueryParameterMapResult.getBaseQueryString());
+
+ // Attach the ordering
+ String queryWithOrdering = attachOrdering(jpqlQuery,
searchParameters.getPageable().getSort());
+
+ // Execute Query
+ TypedQuery<SavingsTransactionSearchResult> queryToExecute =
entityManager.createQuery(queryWithOrdering,
+ SavingsTransactionSearchResult.class);
+ setQueryParameters(queryToExecute,
baseQueryParameterMapResult.getParametersMap());
+ applyPagination(queryToExecute, searchParameters.getPageable());
+ List<SavingsTransactionSearchResult> resultList =
queryToExecute.getResultList();
+
+ // Attach the count selection
+ String countQuery =
attachCountSelection(baseQueryParameterMapResult.getBaseQueryString());
+
+ // Execute count query
+ TypedQuery<Long> countQueryToExecute =
entityManager.createQuery(countQuery, Long.class);
+ setQueryParameters(countQueryToExecute,
baseQueryParameterMapResult.getParametersMap());
+ Long totalElements = countQueryToExecute.getSingleResult();
+
+ return PageableExecutionUtils.getPage(resultList,
searchParameters.getPageable(), () -> totalElements);
+ }
+
+ private <T> void setQueryParameters(TypedQuery<T> queryToExecute,
Map<String, Object> parametersMap) {
+ for (Map.Entry<String, Object> entry : parametersMap.entrySet()) {
+ queryToExecute.setParameter(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private BaseQueryParametersMapResult buildBaseQueryWithFilters(Long
savingsId, DepositAccountType depositAccountType, Filters filters) {
+ String baseQuery = """
+ SELECT tr
+ FROM SavingsAccountTransaction tr
+ JOIN ApplicationCurrency currency ON (currency.code =
tr.savingsAccount.currency.code)
+ LEFT JOIN AccountTransferTransaction fromtran ON
(fromtran.fromSavingsTransaction = tr)
+ LEFT JOIN AccountTransferTransaction totran ON
(totran.toSavingsTransaction = tr)
+ LEFT JOIN tr.notes nt ON (nt.savingsTransaction = tr)
+ WHERE tr.savingsAccount.id = :savingsId
+ AND tr.savingsAccount.depositType = :depositType
+ """;
+ StringBuilder baseQueryBuilder = new StringBuilder(baseQuery);
+
+ Map<String, Object> parameterMap = new HashMap<>();
+ parameterMap.put("savingsId", savingsId);
+ parameterMap.put("depositType", depositAccountType.getValue());
+
+ setFilterConditions(baseQueryBuilder, parameterMap, filters);
+ return new BaseQueryParametersMapResult(baseQueryBuilder.toString(),
parameterMap);
+ }
+
+ private String attachSelection(String baseQuery) {
+ return baseQuery.replace("SELECT tr",
+ "SELECT NEW
org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult(tr.id,tr.typeOf,
tr.dateOf, tr.amount, tr.releaseIdOfHoldAmountTransaction,
tr.reasonForBlock,tr.createdDate, tr.appUser, nt.note, tr.runningBalance,
tr.reversed,tr.reversalTransaction, tr.originalTxnId, tr.lienTransaction,
tr.isManualTransaction,fromTran, toTran, tr.savingsAccount, tr.paymentDetail,
currency) ");
+ }
+
+ private String attachOrdering(String jpqlQuery, Sort sort) {
+ StringJoiner orderByClauseBuilder = new StringJoiner(", ", " ORDER BY
", "");
+
+ if (Objects.nonNull(sort) && sort.isSorted()) {
+ buildOrderByClause(sort.toList(), orderByClauseBuilder);
+ } else {
+ List<Order> defaultOrders = getDefaultOrders();
+ buildOrderByClause(defaultOrders, orderByClauseBuilder);
+ }
+ return new
StringBuilder(jpqlQuery).append(orderByClauseBuilder.toString()).toString();
+ }
+
+ private void buildOrderByClause(List<Order> orders, StringJoiner
orderByClauseBuilder) {
+ for (Order order : orders) {
+ String property = "tr." + order.getProperty();
+ String direction = order.getDirection().name();
+ String orderByExpression = new
StringBuilder(property).append(StringUtils.SPACE).append(direction).toString();
+ orderByClauseBuilder.add(orderByExpression);
+ }
+ }
+
+ private List<Order> getDefaultOrders() {
+ return List.of(Order.desc(TRANSACTION_DATE_FIELD_NAME),
Order.desc(CREATED_DATE_FIELD_NAME), Order.desc(ID_FIELD_NAME));
+ }
+
+ private void applyPagination(TypedQuery<?> query, Pageable pageable) {
+ if (pageable.isPaged()) {
+ query.setFirstResult((int) pageable.getOffset());
+ query.setMaxResults(pageable.getPageSize());
+ }
+ }
+
+ private String attachCountSelection(String baseQuery) {
+ return baseQuery.replace("SELECT tr", "SELECT COUNT(tr) ");
+ }
+
+ private void setFilterConditions(StringBuilder queryBuilder, Map<String,
Object> parameterMap, Filters filters) {
+ if (Objects.nonNull(filters)) {
+ List<RangeFilter<LocalDate>> dateFilters =
filters.getTransactionDate();
+ List<RangeFilter<BigDecimal>> amountFilters =
filters.getTransactionAmount();
+ List<SavingsAccountTransactionType> transactionTypes =
filters.getTransactionType();
+
+ if (Objects.nonNull(dateFilters)) {
+ processRangeFilters(queryBuilder, parameterMap, dateFilters,
TRANSACTION_DATE_FIELD_NAME);
+ }
+
+ if (Objects.nonNull(amountFilters)) {
+ processRangeFilters(queryBuilder, parameterMap, amountFilters,
AMOUNT_FIELD_NAME);
+ }
+
+ if (CollectionUtils.isNotEmpty(transactionTypes)) {
+ List<Integer> transactionTypeValues =
transactionTypes.stream().map(SavingsAccountTransactionType::getValue)
+ .collect(Collectors.toList());
+ queryBuilder.append(" AND tr.typeOf IN :transactionTypes ");
+ parameterMap.put("transactionTypes", transactionTypeValues);
+ }
+ }
+ }
+
+ private <T> void processRangeFilters(StringBuilder queryBuilder,
Map<String, Object> parameterMap, List<RangeFilter<T>> filters,
+ String field) {
+ filters.forEach(filter -> {
+ String paramName = new
StringBuilder(field).append(filter.getOperator()).toString();
+ queryBuilder.append(" AND
tr.").append(field).append(StringUtils.SPACE).append(filter.getOperator().getSymbol()).append("
:")
+ .append(paramName);
+ parameterMap.put(paramName, filter.getValue());
+ });
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java
new file mode 100644
index 000000000..e330aeaed
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.service.search;
+
+import org.apache.fineract.infrastructure.core.service.Page;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+
+public interface SavingsAccountTransactionSearchService {
+
+ Page<SavingsAccountTransactionData> searchTransactions(Long savingsId,
PagedRequest<SavingsTransactionSearch> searchRequest);
+
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java
new file mode 100644
index 000000000..0d477e00c
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.savings.service.search;
+
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.Page;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.portfolio.savings.DepositAccountType;
+import
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+import
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionSearchValidator;
+import
org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult;
+import
org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters;
+import
org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearchParameters;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchServiceImpl implements
SavingsAccountTransactionSearchService {
+
+ private final PlatformSecurityContext context;
+
+ private final SavingsAccountTransactionRepository
savingsTransactionRepository;
+
+ private final SavingsAccountTransactionSearchValidator searchValidator;
+
+ @Override
+ public Page<SavingsAccountTransactionData> searchTransactions(Long
savingsId, PagedRequest<SavingsTransactionSearch> searchRequest) {
+ validateSearchRequest(searchRequest);
+ return executeSearch(savingsId, DepositAccountType.SAVINGS_DEPOSIT,
searchRequest);
+ }
+
+ private void validateSearchRequest(PagedRequest<SavingsTransactionSearch>
searchRequest) {
+ Objects.requireNonNull(searchRequest, "searchRequest must not be
null");
+ context.isAuthenticated();
+ Optional<SavingsTransactionSearch> request =
searchRequest.getRequest();
+ Filters searchFilters =
request.map(SavingsTransactionSearch::getFilters).orElse(null);
+ searchValidator.validateSearchFilters(searchFilters);
+ }
+
+ private Page<SavingsAccountTransactionData> executeSearch(Long savingsId,
DepositAccountType depositType,
+ PagedRequest<SavingsTransactionSearch> searchRequest) {
+ Optional<SavingsTransactionSearch> request =
searchRequest.getRequest();
+ Pageable pageable = searchRequest.toPageable();
+ Filters searchFilters =
request.map(SavingsTransactionSearch::getFilters).orElse(null);
+ SavingsTransactionSearchParameters searchParameters =
SavingsTransactionSearchParameters.builder().savingsId(savingsId)
+
.depositAccountType(DepositAccountType.SAVINGS_DEPOSIT).filters(searchFilters).pageable(pageable).build();
+ org.springframework.data.domain.Page<SavingsAccountTransactionData>
pageResult = savingsTransactionRepository
+
.searchTransactions(searchParameters).map(SavingsTransactionSearchResult::toSavingsAccountTransactionData);
+ return new Page<>(pageResult.getContent(),
Long.valueOf(pageResult.getTotalElements()).intValue());
+ }
+
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java
new file mode 100644
index 000000000..c89440375
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java
@@ -0,0 +1,414 @@
+/**
+ * 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.assertTrue;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.apache.fineract.client.models.Filters;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch;
+import org.apache.fineract.client.models.RangeFilterBigDecimal;
+import org.apache.fineract.client.models.RangeFilterLocalDate;
+import
org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+import org.apache.fineract.client.models.SavingsTransactionSearch;
+import org.apache.fineract.client.models.SortOrder;
+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.Utils;
+import
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
+import
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
+import
org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings({ "rawtypes" })
+public class SavingsAccountTransactionsSearchIntegrationTest {
+
+ public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+ public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy";
+ final String startDate = "01 May 2023";
+ final String firstDepositDate = "05 May 2023";
+ final String secondDepositDate = "09 May 2023";
+ final String thirdDepositDate = "12 May 2023";
+ final String fourthDepositDate = "01 Jun 2023";
+ final String withdrawDate = "10 May 2023";
+
+ private ResponseSpecification responseSpec;
+ private ResponseSpecification responseSpecForValidationError;
+ private RequestSpecification requestSpec;
+ private SavingsProductHelper savingsProductHelper;
+ private SavingsAccountHelper savingsAccountHelper;
+ private SavingsAccountHelper savingsAccountHelperValidationError;
+
+ @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.responseSpecForValidationError = new
ResponseSpecBuilder().expectStatusCode(400).build();
+ this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec,
this.responseSpec);
+ this.savingsAccountHelperValidationError = new
SavingsAccountHelper(this.requestSpec, this.responseSpecForValidationError);
+ this.savingsProductHelper = new SavingsProductHelper();
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchWithAmountFilterLteGte() throws
JsonProcessingException {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE,
BigDecimal.valueOf(100)));
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LTE,
BigDecimal.valueOf(200)));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+ BigDecimal expectedAmount = BigDecimal.valueOf(100);
+ assertEquals(0,
expectedAmount.compareTo(pageItemsList.get(0).getAmount()));
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchWithAmountFilterLtGt() throws
JsonProcessingException {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GT,
BigDecimal.valueOf(100)));
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT,
BigDecimal.valueOf(400)));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+ BigDecimal expectedAmount = BigDecimal.valueOf(300);
+ assertEquals(0,
expectedAmount.compareTo(pageItemsList.get(0).getAmount()));
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchWithDateFilterLteGte() throws
JsonProcessingException {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GTE,
LocalDate.of(2023, 05, 06)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE,
LocalDate.of(2023, 05, 10)));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(2, transactionsResponse.getPageItems().size());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+ assertEquals(0, parseDate(withdrawDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate()));
+ assertEquals(0, parseDate(secondDepositDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate()));
+ }
+
+ @Test
+ public void
testSavingsTransactionsSearchWithTransactionTypeDepositAndDefaultSort() {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(2, transactionsResponse.getPageItems().size());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(300).compareTo(pageItemsList.get(0).getAmount()));
+ assertEquals(0, parseDate(secondDepositDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate()));
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(100).compareTo(pageItemsList.get(1).getAmount()));
+ assertEquals(0, parseDate(firstDepositDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate()));
+ }
+
+ @Test
+ public void
testSavingsTransactionsSearchWithTransactionTypeWithdrawAndDeposit() throws
JsonProcessingException {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT,
Filters.TransactionTypeEnum.WITHDRAWAL));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(3, transactionsResponse.getPageItems().size());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+ assertTrue(
+
Filters.TransactionTypeEnum.WITHDRAWAL.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount()));
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount()));
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(2).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(100).compareTo(pageItemsList.get(2).getAmount()));
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchWithPaginationAndNoFilter() {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ Filters filters = new Filters();
+ int page = 0;
+ int size = 2;
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, page, size, null);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(2, transactionsResponse.getPageItems().size());
+ }
+
+ @Test
+ public void
testSavingsTransactionsSearchWithTransactionTypeDepositAndSortByAmountAsc() {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"200", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ Filters filters = new Filters();
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
+ SortOrder sortOrder = new SortOrder();
+ sortOrder.setProperty("amount");
+ sortOrder.setDirection(SortOrder.DirectionEnum.ASC);
+ List<SortOrder> sortOrders = List.of(sortOrder);
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, sortOrders);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(2, transactionsResponse.getPageItems().size());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount()));
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount()));
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchWithFiltersSortingAndPagination()
{
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400",
thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200",
fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ this.savingsAccountHelper.postInterestForSavings(savingsId);
+ Filters filters = new Filters();
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE,
BigDecimal.valueOf(100)));
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT,
BigDecimal.valueOf(500)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT,
LocalDate.of(2023, 05, 06)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE,
LocalDate.of(2023, 06, 01)));
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
+ SortOrder sortOrder = new SortOrder();
+ sortOrder.setProperty("amount");
+ sortOrder.setDirection(SortOrder.DirectionEnum.DESC);
+ List<SortOrder> sortOrders = List.of(sortOrder);
+ int page = 0;
+ int size = 2;
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, page, size, sortOrders);
+ SavingsAccountTransactionsSearchResponse transactionsResponse =
this.savingsAccountHelper.searchTransactions(savingsId,
+ searchRequest);
+ Assertions.assertNotNull(transactionsResponse);
+ assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+ Assertions.assertNotNull(transactionsResponse.getPageItems());
+ assertEquals(2, transactionsResponse.getPageItems().size());
+ List<GetSavingsAccountTransactionsPageItem> pageItemsList =
List.copyOf(transactionsResponse.getPageItems());
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(400).compareTo(pageItemsList.get(0).getAmount()));
+ assertEquals(0, parseDate(thirdDepositDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate()));
+
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue()));
+ assertEquals(0,
BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount()));
+ assertEquals(0, parseDate(secondDepositDate,
DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate()));
+
+ }
+
+ @Test
+ public void testSavingsTransactionsSearchFilterRangeValidationError() {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400",
thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200",
fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE,
BigDecimal.valueOf(100)));
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT,
BigDecimal.valueOf(500)));
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT,
BigDecimal.valueOf(1000)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT,
LocalDate.of(2023, 05, 6)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE,
LocalDate.of(2023, 06, 1)));
+
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE,
LocalDate.of(2023, 06, 8)));
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
+ SortOrder sortOrder = new SortOrder();
+ sortOrder.setProperty("amount");
+ sortOrder.setDirection(SortOrder.DirectionEnum.DESC);
+ List<SortOrder> sortOrders = List.of(sortOrder);
+ int page = 0;
+ int size = 2;
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, page, size, sortOrders);
+ this.savingsAccountHelperValidationError.searchTransactions(savingsId,
searchRequest);
+ }
+
+ @Test
+ public void
testSavingsTransactionsSearchTransactionAmountValidationError() {
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec, startDate);
+ Assertions.assertNotNull(clientID);
+ final Integer savingsId = createSavingsAccountDailyPosting(clientID,
startDate);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300",
secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId,
"50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400",
thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200",
fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ Filters filters = new Filters();
+
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE,
BigDecimal.valueOf(-100)));
+
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
+ PagedRequestSavingsTransactionSearch searchRequest =
buildTransactionsSearchReqeust(filters, null, null, null);
+ this.savingsAccountHelperValidationError.searchTransactions(savingsId,
searchRequest);
+ }
+
+ private Integer createSavingsAccountDailyPosting(final Integer clientID,
final String startDate) {
+ final Integer savingsProductID = createSavingsProductDailyPosting();
+ Assertions.assertNotNull(savingsProductID);
+ final Integer savingsId =
this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID,
savingsProductID,
+ ACCOUNT_TYPE_INDIVIDUAL, startDate);
+ Assertions.assertNotNull(savingsId);
+ HashMap savingsStatusHashMap =
this.savingsAccountHelper.approveSavingsOnDate(savingsId, startDate);
+ SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
+ savingsStatusHashMap =
this.savingsAccountHelper.activateSavingsAccount(savingsId, startDate);
+ SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
+ return savingsId;
+ }
+
+ private Integer createSavingsProductDailyPosting() {
+ final String savingsProductJSON =
this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily()
+
.withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build();
+ return SavingsProductHelper.createSavingsProduct(savingsProductJSON,
requestSpec, responseSpec);
+ }
+
+ private PagedRequestSavingsTransactionSearch
buildTransactionsSearchReqeust(Filters filters, Integer page, Integer size,
+ List<SortOrder> sorts) {
+ final Integer DEFAULT_PAGE_SIZE = 50;
+ SavingsTransactionSearch savingsTransactionSearch = new
SavingsTransactionSearch();
+ savingsTransactionSearch.setFilters(filters);
+ PagedRequestSavingsTransactionSearch pagedRequest = new
PagedRequestSavingsTransactionSearch();
+ pagedRequest.setRequest(savingsTransactionSearch);
+ pagedRequest.setSorts(sorts != null ? sorts : new ArrayList<>());
+ pagedRequest.setPage(page != null ? page : 0);
+ pagedRequest.setSize(size != null ? size : DEFAULT_PAGE_SIZE);
+ return pagedRequest;
+ }
+
+ private RangeFilterBigDecimal
buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum operator,
BigDecimal value) {
+ RangeFilterBigDecimal transactionAmountFilter = new
RangeFilterBigDecimal();
+ transactionAmountFilter.setOperator(operator);
+ transactionAmountFilter.setValue(value);
+ return transactionAmountFilter;
+ }
+
+ private RangeFilterLocalDate
buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum operator, LocalDate
value) {
+ RangeFilterLocalDate transactionDateFilter = new
RangeFilterLocalDate();
+ transactionDateFilter.setOperator(operator);
+ transactionDateFilter.setValue(value);
+ return transactionDateFilter;
+ }
+
+ public static LocalDate parseDate(String dateStr, String pattern) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+ return LocalDate.parse(dateStr, formatter);
+ }
+
+ // Reset configuration fields
+ @AfterEach
+ public void tearDown() {
+
GlobalConfigurationHelper.resetAllDefaultGlobalConfigurations(this.requestSpec,
this.responseSpec);
+
GlobalConfigurationHelper.verifyAllDefaultGlobalConfigurations(this.requestSpec,
this.responseSpec);
+ }
+
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
index 1c8f66585..7f6cc2d0f 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
@@ -36,6 +36,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch;
+import
org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -49,6 +52,7 @@ public class SavingsAccountHelper {
private final RequestSpecification requestSpec;
private final ResponseSpecification responseSpec;
+ private static final Gson GSON = new JSON().getGson();
private static final Logger LOG =
LoggerFactory.getLogger(SavingsAccountHelper.class);
private static final String SAVINGS_ACCOUNT_URL =
"/fineract-provider/api/v1/savingsaccounts";
@@ -646,6 +650,15 @@ public class SavingsAccountHelper {
return Utils.performServerGet(requestSpec, responseSpec, URL, "");
}
+ public SavingsAccountTransactionsSearchResponse searchTransactions(Integer
savingsId,
+ PagedRequestSavingsTransactionSearch searchReqeust) {
+ final String SAVINGS_TRANSACTIONS_SEARCH_URL = SAVINGS_ACCOUNT_URL +
"/" + savingsId + "/transactions/search" + "?"
+ + Utils.TENANT_IDENTIFIER;
+ String jsonBodyToSend = GSON.toJson(searchReqeust);
+ String response = Utils.performServerPost(this.requestSpec,
this.responseSpec, SAVINGS_TRANSACTIONS_SEARCH_URL, jsonBodyToSend);
+ return GSON.fromJson(response,
SavingsAccountTransactionsSearchResponse.class);
+ }
+
public List<HashMap> getSavingsTransactions(final Integer savingsID) {
final Object get = getSavingsCollectionAttribute(savingsID,
"transactions");
final String json = new Gson().toJson(get);