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);


Reply via email to