You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by "Mk9894 (via GitHub)" <gi...@apache.org> on 2023/05/22 14:45:40 UTC

[GitHub] [fineract] Mk9894 opened a new pull request, #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Mk9894 opened a new pull request, #3192:
URL: https://github.com/apache/fineract/pull/3192

   ## Description
   
   Describe the changes made and why they were made.
   
   Ignore if these details are present on the associated [Apache Fineract JIRA ticket](https://github.com/apache/fineract/pull/1284).
   
   
   ## Checklist
   
   Please make sure these boxes are checked before submitting your pull request - thanks!
   
   - [x] Write the commit message as per https://github.com/apache/fineract/#pull-requests
   
   - [x] Acknowledge that we will not review PRs that are not passing the build _("green")_ - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
   
   - [x] Create/update unit or integration tests for verifying the changes made.
   
   - [x] Follow coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions.
   
   - [x] Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes
   
   - [x] Submission is not a "code dump".  (Large changes can be made "in repository" via a branch.  Ask on the developer mailing list for guidance, if required.)
   
   FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206651253


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -126,7 +141,7 @@ public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte
             final ChargeReadPlatformService chargeReadPlatformService,
             final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator,
             final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper,
-            DatabaseSpecificSQLGenerator sqlGenerator) {
+            DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountTransactionRepository savingsAccountTransactionsRepository) {

Review Comment:
   This is a constructor. If it breaks the existing impl, you'll know it because the startup of the Spring context will simply fail.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217898985


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   Done.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206563735


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java:
##########
@@ -53,4 +56,28 @@ List<SavingsAccountTransaction> findTransactionRunningBalanceBeforePivotDate(@Pa
     @Query("select sat from SavingsAccountTransaction sat where sat.savingsAccount.id = :savingsId and sat.dateOf <= :transactionDate and sat.reversed=false")
     List<SavingsAccountTransaction> findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(@Param("savingsId") Long savingsId,
             @Param("transactionDate") LocalDate transactionDate, Pageable pageable);
+
+    @Query("""
+                    SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTOV2(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
+                )
+                    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
+                    AND (:transactionType IS NULL OR tr.typeOf = :transactionType )
+                    AND (:#{#searchParameters.fromDate} IS NULL OR tr.dateOf >= :#{#searchParameters.fromDate} )
+                    AND (:#{#searchParameters.toDate} IS NULL OR tr.dateOf <= :#{#searchParameters.toDate} )
+                    AND (:#{#searchParameters.fromAmount} IS NULL OR tr.amount >= :#{#searchParameters.fromAmount} )
+                    AND (:#{#searchParameters.toAmount} IS NULL OR tr.amount <= :#{#searchParameters.toAmount} )
+            """)
+    Page<SavingsAccountTransactionDTOV2> findAll(@Param("savingsId") Long savingsId, @Param("depositType") Integer depositType,

Review Comment:
   @galovics As per my current implementation findAll will also work. If you dont provide any query param to this endpoint, still you can get all the transactions without any filter applied to the result set! By adding query params you can get the filtered records.
   
   If i misunderstood the requirement, then Is it okay to implement a POST endpoint for this ?
   Eg: POST /savingsaccount/{id}/transactions/search
   
   Please confirm.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1219410650


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java:
##########
@@ -0,0 +1,80 @@
+/**
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Data;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+@Data
+public class SavingsTransactionSearch {

Review Comment:
   I don't agree with this structure at all.
   If I translate this to JSON, looks like this:
   
   ```
   {
     "filters": {
        "transactionDate": {
           "gte": "2022-01-01"
        }
     }
   }
   ```
   While it should be something like:
   ```
   {
     "filters": {
        "transactionDate": {
           "operator": "gte",
           "value": "2022-01-01"
        }
     }
   }
   ```
   
   Much more readable, maintainable and extendable this way.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,

Review Comment:
   Let's work with types, why don't we use a DepositAccountType enum here?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,
+            Pageable pageable) {
+        String jpql = buildJpqlQuery();

Review Comment:
   The whole implementation lacks structure.
   
   I'd imagine on a high level this:
   
   1. Build base query with filters but without the selection
   2. Attach the selection
   3. Attach the ordering
   4. Execute the query
   5. Build base query with filters
   6. Attach the COUNT selection
   7. Execute the query (with the PageExecutionUtils..)
   8. Construct the page object



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java:
##########
@@ -0,0 +1,386 @@
+/**
+ * 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.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.client.models.AmountFilters;
+import org.apache.fineract.client.models.DateFilters;
+import org.apache.fineract.client.models.Filters;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+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";
+    final String startDate = "01 May 2023";
+    final String firstDepositDate = "05 May 2023";
+    final String secondDepositDate = "09 May 2023";
+    final String withdrawDate = "10 May 2023";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGte(BigDecimal.valueOf(100));
+        amountFilters.setLte(BigDecimal.valueOf(200));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(100).setScale(6);

Review Comment:
   BigDecimals shouldn't be compared with equals but rather compareTo.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java:
##########
@@ -0,0 +1,386 @@
+/**
+ * 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.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.client.models.AmountFilters;
+import org.apache.fineract.client.models.DateFilters;
+import org.apache.fineract.client.models.Filters;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+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";
+    final String startDate = "01 May 2023";
+    final String firstDepositDate = "05 May 2023";
+    final String secondDepositDate = "09 May 2023";
+    final String withdrawDate = "10 May 2023";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGte(BigDecimal.valueOf(100));
+        amountFilters.setLte(BigDecimal.valueOf(200));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(100).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGt(BigDecimal.valueOf(100));
+        amountFilters.setLt(BigDecimal.valueOf(400));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(300).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        DateFilters dateFilters = new DateFilters();
+        dateFilters.setGte(LocalDate.of(2023, 05, 06));
+        dateFilters.setLte(LocalDate.of(2023, 05, 10));
+        filters.setTransactionDate(dateFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> expectedDates = List.of(LocalDate.parse(secondDepositDate, DateTimeFormatter.ofPattern("dd MMM yyyy")),
+                LocalDate.parse(withdrawDate, DateTimeFormatter.ofPattern("dd MMM yyyy")));
+        pageItems.forEach(data -> {
+            assertTrue(expectedDates.contains(data.getDate()));
+        });
+    }
+
+    @Test
+    public void testSavingsTransactionsSearchWithTransactionTypeDeposit() 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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            assertEquals(true, data.getTransactionType().getDeposit());
+        });
+    }
+
+    @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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            List<String> expectedTransactionTypes = List.of(Filters.TransactionTypeEnum.DEPOSIT.getValue(),
+                    Filters.TransactionTypeEnum.WITHDRAWAL.getValue());
+            assertTrue(expectedTransactionTypes.contains(data.getTransactionType().getValue().toUpperCase()));
+        });
+    }
+
+    @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);
+        this.savingsAccountHelper.postInterestForSavings(savingsId);
+        Filters filters = new Filters();
+        int page = 0;
+        int size = 10;
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters,
+                null, page, size);
+        Assertions.assertNotNull(transactionsResponse);
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(10, transactionsResponse.getPageItems().size());
+    }
+
+    @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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        List<LocalDate> transactionDates = transactionsResponse.getPageItems().stream().map(data -> data.getDate())
+                .collect(Collectors.toList());
+        assertTrue(isDateListOrdered(transactionDates, "desc"));

Review Comment:
   Why don't you check the individual items one by one in the proper order?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,
+            Pageable pageable) {
+        String jpql = buildJpqlQuery();
+        StringBuilder queryBuilder = new StringBuilder(jpql);
+        queryBuilder.append(" WHERE ");
+        queryBuilder.append(" tr.savingsAccount.id = :savingsId ");
+        queryBuilder.append(" AND tr.savingsAccount.depositType = :depositType ");
+        Map<String, Object> parameterMap = new HashMap<>();
+        parameterMap.put("savingsId", savingsId);
+        parameterMap.put("depositType", depositAccountType);
+
+        if (Objects.nonNull(filters)) {
+            SavingsTransactionSearch.DateFilters dateFilters = filters.getDateFilters();
+            if (Objects.nonNull(dateFilters)) {
+                if (Objects.nonNull(dateFilters.getLte())) {
+                    queryBuilder.append(" AND tr.dateOf <= :dateLte ");
+                    parameterMap.put("dateLte", dateFilters.getLte());
+                }
+                if (Objects.nonNull(dateFilters.getGte())) {
+                    queryBuilder.append(" AND tr.dateOf >= :dateGte ");
+                    parameterMap.put("dateGte", dateFilters.getGte());
+                }
+                // We prioritize the lte/gte filter by applying it first. If lte/gte is not provided, only then we

Review Comment:
   All this can be removed if you do the structure I mentioned above.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java:
##########
@@ -0,0 +1,80 @@
+/**
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Data;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+@Data
+public class SavingsTransactionSearch {

Review Comment:
   I don't get why you need the JsonProperty annotations everywhere. Why don't you just name the attributes appropriately?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,

Review Comment:
   Why don't we use a Parameter Object here instead of the individual params?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,
+            Pageable pageable) {
+        String jpql = buildJpqlQuery();
+        StringBuilder queryBuilder = new StringBuilder(jpql);
+        queryBuilder.append(" WHERE ");
+        queryBuilder.append(" tr.savingsAccount.id = :savingsId ");
+        queryBuilder.append(" AND tr.savingsAccount.depositType = :depositType ");
+        Map<String, Object> parameterMap = new HashMap<>();
+        parameterMap.put("savingsId", savingsId);
+        parameterMap.put("depositType", depositAccountType);
+
+        if (Objects.nonNull(filters)) {
+            SavingsTransactionSearch.DateFilters dateFilters = filters.getDateFilters();
+            if (Objects.nonNull(dateFilters)) {
+                if (Objects.nonNull(dateFilters.getLte())) {
+                    queryBuilder.append(" AND tr.dateOf <= :dateLte ");
+                    parameterMap.put("dateLte", dateFilters.getLte());
+                }
+                if (Objects.nonNull(dateFilters.getGte())) {
+                    queryBuilder.append(" AND tr.dateOf >= :dateGte ");
+                    parameterMap.put("dateGte", dateFilters.getGte());
+                }
+                // We prioritize the lte/gte filter by applying it first. If lte/gte is not provided, only then we
+                // apply the lt/gt filter.
+                // By enforcing this order of conditions, we ensure that if both lte/gte and lt/gt are provided, the
+                // lte/gte condition takes precedence and the lt/gt condition is ignored. This helps avoid
+                // conflicting or redundant filter conditions and provides a consistent behavior for the users.
+                if (Objects.isNull(dateFilters.getLte()) && Objects.nonNull(dateFilters.getLt())) {
+                    queryBuilder.append(" AND tr.dateOf < :dateLt ");
+                    parameterMap.put("dateLt", dateFilters.getLt());
+                }
+                if (Objects.isNull(dateFilters.getGte()) && Objects.nonNull(dateFilters.getGt())) {
+                    queryBuilder.append(" AND tr.dateOf > :dateGt ");
+                    parameterMap.put("dateGt", dateFilters.getGt());
+                }
+            }
+
+            SavingsTransactionSearch.AmountFilters amountFilters = filters.getAmountFilters();
+            if (Objects.nonNull(amountFilters)) {
+                if (Objects.nonNull(amountFilters.getLte())) {
+                    queryBuilder.append(" AND tr.amount <= :amountLte ");
+                    parameterMap.put("amountLte", amountFilters.getLte());
+                }
+                if (Objects.nonNull(amountFilters.getGte())) {
+                    queryBuilder.append(" AND tr.amount >= :amountGte ");
+                    parameterMap.put("amountGte", amountFilters.getGte());
+                }
+                // We prioritize the lte/gte filter by applying it first. If lte/gte is not provided, only then we
+                // apply the lt/gt filter.
+                // By enforcing this order of conditions, we ensure that if both lte/gte and lt/gt are provided, the
+                // lte/gte condition takes precedence and the lt/gt condition is ignored. This helps avoid
+                // conflicting or redundant filter conditions and provides a consistent behavior for the users.
+                if (Objects.isNull(amountFilters.getLte()) && Objects.nonNull(amountFilters.getLt())) {
+                    queryBuilder.append(" AND tr.amount < :amountLt ");
+                    parameterMap.put("amountLt", amountFilters.getLt());
+                }
+                if (Objects.isNull(amountFilters.getGte()) && Objects.nonNull(amountFilters.getGt())) {
+                    queryBuilder.append(" AND tr.amount > :amountGt ");
+                    parameterMap.put("amountGt", amountFilters.getGt());
+                }
+            }
+            List<SavingsAccountTransactionType> transactionTypes = filters.getTransactionType();
+            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);
+            }
+        }
+        Sort sortFromPageable = pageable.getSort();
+        if (Objects.nonNull(pageable.getSort()) && pageable.getSort().isSorted()) {
+            queryBuilder.append(getSortOrders(sortFromPageable));
+        }
+
+        TypedQuery<SavingsTransactionSearchResult> queryToExecute = entityManager.createQuery(queryBuilder.toString(),
+                SavingsTransactionSearchResult.class);
+
+        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
+            queryToExecute.setParameter(entry.getKey(), entry.getValue());
+        }
+        if (pageable.isPaged()) {
+            queryToExecute.setFirstResult((int) pageable.getOffset());
+            queryToExecute.setMaxResults(pageable.getPageSize());
+        }
+        List<SavingsTransactionSearchResult> resultList = queryToExecute.getResultList();
+        return PageableExecutionUtils.getPage(resultList, pageable, () -> getTotalElements(queryBuilder, parameterMap));
+    }
+
+    private String buildJpqlQuery() {
+        return """
+                        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
+                    )
+                        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)
+                """;
+    }
+
+    private Long getTotalElements(StringBuilder queryBuilder, Map<String, Object> parameterMap) {
+        // ORDER BY Clause not required for Count Query
+        int orderByIndex = queryBuilder.indexOf("ORDER BY");
+        if (orderByIndex != -1) {
+            queryBuilder.replace(orderByIndex, queryBuilder.length(), "");
+        }
+        String countJPQL = "SELECT COUNT(tr) " + queryBuilder.substring(queryBuilder.indexOf("FROM"), queryBuilder.length());

Review Comment:
   No, please no. No string manipulation. As soon as you start doing it, there's never a stop to it. Really difficult to refactor and maintain + as I said above, extremely fragile.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.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 final EntityManager entityManager;
+
+    @Override
+    public Page<SavingsTransactionSearchResult> searchTransactions(Long savingsId, Integer depositAccountType, Filters filters,
+            Pageable pageable) {
+        String jpql = buildJpqlQuery();
+        StringBuilder queryBuilder = new StringBuilder(jpql);
+        queryBuilder.append(" WHERE ");
+        queryBuilder.append(" tr.savingsAccount.id = :savingsId ");
+        queryBuilder.append(" AND tr.savingsAccount.depositType = :depositType ");
+        Map<String, Object> parameterMap = new HashMap<>();
+        parameterMap.put("savingsId", savingsId);
+        parameterMap.put("depositType", depositAccountType);
+
+        if (Objects.nonNull(filters)) {
+            SavingsTransactionSearch.DateFilters dateFilters = filters.getDateFilters();
+            if (Objects.nonNull(dateFilters)) {
+                if (Objects.nonNull(dateFilters.getLte())) {
+                    queryBuilder.append(" AND tr.dateOf <= :dateLte ");
+                    parameterMap.put("dateLte", dateFilters.getLte());
+                }
+                if (Objects.nonNull(dateFilters.getGte())) {
+                    queryBuilder.append(" AND tr.dateOf >= :dateGte ");
+                    parameterMap.put("dateGte", dateFilters.getGte());
+                }
+                // We prioritize the lte/gte filter by applying it first. If lte/gte is not provided, only then we
+                // apply the lt/gt filter.
+                // By enforcing this order of conditions, we ensure that if both lte/gte and lt/gt are provided, the
+                // lte/gte condition takes precedence and the lt/gt condition is ignored. This helps avoid
+                // conflicting or redundant filter conditions and provides a consistent behavior for the users.
+                if (Objects.isNull(dateFilters.getLte()) && Objects.nonNull(dateFilters.getLt())) {
+                    queryBuilder.append(" AND tr.dateOf < :dateLt ");
+                    parameterMap.put("dateLt", dateFilters.getLt());
+                }
+                if (Objects.isNull(dateFilters.getGte()) && Objects.nonNull(dateFilters.getGt())) {
+                    queryBuilder.append(" AND tr.dateOf > :dateGt ");
+                    parameterMap.put("dateGt", dateFilters.getGt());
+                }
+            }
+
+            SavingsTransactionSearch.AmountFilters amountFilters = filters.getAmountFilters();
+            if (Objects.nonNull(amountFilters)) {
+                if (Objects.nonNull(amountFilters.getLte())) {
+                    queryBuilder.append(" AND tr.amount <= :amountLte ");
+                    parameterMap.put("amountLte", amountFilters.getLte());
+                }
+                if (Objects.nonNull(amountFilters.getGte())) {
+                    queryBuilder.append(" AND tr.amount >= :amountGte ");
+                    parameterMap.put("amountGte", amountFilters.getGte());
+                }
+                // We prioritize the lte/gte filter by applying it first. If lte/gte is not provided, only then we
+                // apply the lt/gt filter.
+                // By enforcing this order of conditions, we ensure that if both lte/gte and lt/gt are provided, the
+                // lte/gte condition takes precedence and the lt/gt condition is ignored. This helps avoid
+                // conflicting or redundant filter conditions and provides a consistent behavior for the users.
+                if (Objects.isNull(amountFilters.getLte()) && Objects.nonNull(amountFilters.getLt())) {
+                    queryBuilder.append(" AND tr.amount < :amountLt ");
+                    parameterMap.put("amountLt", amountFilters.getLt());
+                }
+                if (Objects.isNull(amountFilters.getGte()) && Objects.nonNull(amountFilters.getGt())) {
+                    queryBuilder.append(" AND tr.amount > :amountGt ");
+                    parameterMap.put("amountGt", amountFilters.getGt());
+                }
+            }
+            List<SavingsAccountTransactionType> transactionTypes = filters.getTransactionType();
+            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);
+            }
+        }
+        Sort sortFromPageable = pageable.getSort();
+        if (Objects.nonNull(pageable.getSort()) && pageable.getSort().isSorted()) {
+            queryBuilder.append(getSortOrders(sortFromPageable));
+        }
+
+        TypedQuery<SavingsTransactionSearchResult> queryToExecute = entityManager.createQuery(queryBuilder.toString(),
+                SavingsTransactionSearchResult.class);
+
+        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
+            queryToExecute.setParameter(entry.getKey(), entry.getValue());
+        }
+        if (pageable.isPaged()) {
+            queryToExecute.setFirstResult((int) pageable.getOffset());
+            queryToExecute.setMaxResults(pageable.getPageSize());
+        }
+        List<SavingsTransactionSearchResult> resultList = queryToExecute.getResultList();
+        return PageableExecutionUtils.getPage(resultList, pageable, () -> getTotalElements(queryBuilder, parameterMap));
+    }
+
+    private String buildJpqlQuery() {
+        return """
+                        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
+                    )
+                        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)
+                """;
+    }
+
+    private Long getTotalElements(StringBuilder queryBuilder, Map<String, Object> parameterMap) {
+        // ORDER BY Clause not required for Count Query
+        int orderByIndex = queryBuilder.indexOf("ORDER BY");

Review Comment:
   This is a super no-go. Why construct the order by in there in the first place? What if the ORDER BY clause is all lowercase? This is extremely fragile.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java:
##########
@@ -118,6 +124,19 @@ public String retrieveOne(@PathParam("savingsId") final Long savingsId, @PathPar
                 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,

Review Comment:
   Is there any reason we must return a String from this instead of the Page itself? 



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java:
##########
@@ -0,0 +1,74 @@
+/**
+ * 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.List;
+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.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.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchServiceImpl {

Review Comment:
   Class shouldn't be named as Impl since there's no interface behind it.



##########
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java:
##########
@@ -53,6 +55,18 @@ public Pageable toPageable() {
         }
     }
 
+    public Pageable toPageableWithDefaultSortOrders(List<Order> defaultSortOrders) {

Review Comment:
   This is mostly copy paste and as I said on the service class, the default ordering shouldn't be on this level but rather on the repo.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java:
##########
@@ -0,0 +1,28 @@
+/**
+ * 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.PagedRequest;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+
+public interface SavingsAccountTransactionSearchService {

Review Comment:
   Interface is not used.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java:
##########
@@ -0,0 +1,74 @@
+/**
+ * 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.List;
+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.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.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchServiceImpl {
+
+    private final PlatformSecurityContext context;
+
+    private final SavingsAccountTransactionRepository savingsTransactionRepository;
+
+    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();
+    }
+
+    private Page<SavingsAccountTransactionData> executeSearch(Long savingsId, DepositAccountType depositType,
+            PagedRequest<SavingsTransactionSearch> searchRequest) {
+        Optional<SavingsTransactionSearch> request = searchRequest.getRequest();
+        Pageable pageable = searchRequest.toPageableWithDefaultSortOrders(getDefaultOrders());
+        Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null);
+        org.springframework.data.domain.Page<SavingsAccountTransactionData> pageResult = savingsTransactionRepository
+                .searchTransactions(savingsId, depositType.getValue(), searchFilters, pageable)
+                .map(SavingsTransactionSearchResult::toSavingsAccountTransactionData);
+        return new Page<>(pageResult.getContent(), Long.valueOf(pageResult.getTotalElements()).intValue());
+    }
+
+    private List<Order> getDefaultOrders() {

Review Comment:
   I don't think putting the default ordering on is the responsibility of the service but rather the repository layer since it has to make sure for a paged query, there must be a consistent ordering in place.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java:
##########
@@ -0,0 +1,386 @@
+/**
+ * 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.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.client.models.AmountFilters;
+import org.apache.fineract.client.models.DateFilters;
+import org.apache.fineract.client.models.Filters;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+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";
+    final String startDate = "01 May 2023";
+    final String firstDepositDate = "05 May 2023";
+    final String secondDepositDate = "09 May 2023";
+    final String withdrawDate = "10 May 2023";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGte(BigDecimal.valueOf(100));
+        amountFilters.setLte(BigDecimal.valueOf(200));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(100).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGt(BigDecimal.valueOf(100));
+        amountFilters.setLt(BigDecimal.valueOf(400));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(300).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        DateFilters dateFilters = new DateFilters();
+        dateFilters.setGte(LocalDate.of(2023, 05, 06));
+        dateFilters.setLte(LocalDate.of(2023, 05, 10));
+        filters.setTransactionDate(dateFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> expectedDates = List.of(LocalDate.parse(secondDepositDate, DateTimeFormatter.ofPattern("dd MMM yyyy")),
+                LocalDate.parse(withdrawDate, DateTimeFormatter.ofPattern("dd MMM yyyy")));
+        pageItems.forEach(data -> {
+            assertTrue(expectedDates.contains(data.getDate()));
+        });
+    }
+
+    @Test
+    public void testSavingsTransactionsSearchWithTransactionTypeDeposit() 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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            assertEquals(true, data.getTransactionType().getDeposit());
+        });
+    }
+
+    @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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            List<String> expectedTransactionTypes = List.of(Filters.TransactionTypeEnum.DEPOSIT.getValue(),
+                    Filters.TransactionTypeEnum.WITHDRAWAL.getValue());
+            assertTrue(expectedTransactionTypes.contains(data.getTransactionType().getValue().toUpperCase()));
+        });
+    }
+
+    @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);
+        this.savingsAccountHelper.postInterestForSavings(savingsId);
+        Filters filters = new Filters();
+        int page = 0;
+        int size = 10;
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters,
+                null, page, size);
+        Assertions.assertNotNull(transactionsResponse);
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(10, transactionsResponse.getPageItems().size());

Review Comment:
   This test is not following the FIRST principles, specifically the I and R are violated since you're expecting 10 savings transactions but you're only saving 3 transactions + the interest posting which I assume is only 1 transaction.
   So if this test is being ran by itself, this will fail.
   
   Note: unless the post savings interest thingy creates 7+ transactions, then just disregard my comment.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java:
##########
@@ -646,6 +653,47 @@ public HashMap getSavingsTransaction(final Integer savingsID, final Integer savi
         return Utils.performServerGet(requestSpec, responseSpec, URL, "");
     }
 
+    public SavingsAccountTransactionsSearchResponse searchTransactions(Integer savingsId, Filters filters) {

Review Comment:
   These methods are copy paste. Why don't you reuse them properly?



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java:
##########
@@ -0,0 +1,386 @@
+/**
+ * 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.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.client.models.AmountFilters;
+import org.apache.fineract.client.models.DateFilters;
+import org.apache.fineract.client.models.Filters;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
+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";
+    final String startDate = "01 May 2023";
+    final String firstDepositDate = "05 May 2023";
+    final String secondDepositDate = "09 May 2023";
+    final String withdrawDate = "10 May 2023";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGte(BigDecimal.valueOf(100));
+        amountFilters.setLte(BigDecimal.valueOf(200));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(100).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        AmountFilters amountFilters = new AmountFilters();
+        amountFilters.setGt(BigDecimal.valueOf(100));
+        amountFilters.setLt(BigDecimal.valueOf(400));
+        filters.setAmount(amountFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            BigDecimal expectedAmount = BigDecimal.valueOf(300).setScale(6);
+            assertEquals(expectedAmount, data.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();
+        DateFilters dateFilters = new DateFilters();
+        dateFilters.setGte(LocalDate.of(2023, 05, 06));
+        dateFilters.setLte(LocalDate.of(2023, 05, 10));
+        filters.setTransactionDate(dateFilters);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> expectedDates = List.of(LocalDate.parse(secondDepositDate, DateTimeFormatter.ofPattern("dd MMM yyyy")),
+                LocalDate.parse(withdrawDate, DateTimeFormatter.ofPattern("dd MMM yyyy")));
+        pageItems.forEach(data -> {
+            assertTrue(expectedDates.contains(data.getDate()));
+        });
+    }
+
+    @Test
+    public void testSavingsTransactionsSearchWithTransactionTypeDeposit() 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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            assertEquals(true, data.getTransactionType().getDeposit());
+        });
+    }
+
+    @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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        pageItems.forEach(data -> {
+            List<String> expectedTransactionTypes = List.of(Filters.TransactionTypeEnum.DEPOSIT.getValue(),
+                    Filters.TransactionTypeEnum.WITHDRAWAL.getValue());
+            assertTrue(expectedTransactionTypes.contains(data.getTransactionType().getValue().toUpperCase()));
+        });
+    }
+
+    @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);
+        this.savingsAccountHelper.postInterestForSavings(savingsId);
+        Filters filters = new Filters();
+        int page = 0;
+        int size = 10;
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters,
+                null, page, size);
+        Assertions.assertNotNull(transactionsResponse);
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(10, transactionsResponse.getPageItems().size());
+    }
+
+    @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));
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        List<LocalDate> transactionDates = transactionsResponse.getPageItems().stream().map(data -> data.getDate())
+                .collect(Collectors.toList());
+        assertTrue(isDateListOrdered(transactionDates, "desc"));
+    }
+
+    @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, "100", 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);
+        SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, filters,
+                sortOrders);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        List<BigDecimal> amountList = transactionsResponse.getPageItems().stream().map(data -> data.getAmount())
+                .collect(Collectors.toList());
+        assertTrue(isListOrdered(amountList, "asc"));

Review Comment:
   Same as above.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206654895


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java:
##########
@@ -53,4 +56,28 @@ List<SavingsAccountTransaction> findTransactionRunningBalanceBeforePivotDate(@Pa
     @Query("select sat from SavingsAccountTransaction sat where sat.savingsAccount.id = :savingsId and sat.dateOf <= :transactionDate and sat.reversed=false")
     List<SavingsAccountTransaction> findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(@Param("savingsId") Long savingsId,
             @Param("transactionDate") LocalDate transactionDate, Pageable pageable);
+
+    @Query("""
+                    SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTOV2(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
+                )
+                    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
+                    AND (:transactionType IS NULL OR tr.typeOf = :transactionType )
+                    AND (:#{#searchParameters.fromDate} IS NULL OR tr.dateOf >= :#{#searchParameters.fromDate} )
+                    AND (:#{#searchParameters.toDate} IS NULL OR tr.dateOf <= :#{#searchParameters.toDate} )
+                    AND (:#{#searchParameters.fromAmount} IS NULL OR tr.amount >= :#{#searchParameters.fromAmount} )
+                    AND (:#{#searchParameters.toAmount} IS NULL OR tr.amount <= :#{#searchParameters.toAmount} )
+            """)
+    Page<SavingsAccountTransactionDTOV2> findAll(@Param("savingsId") Long savingsId, @Param("depositType") Integer depositType,

Review Comment:
   Sure. Thanks for the heads up!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] jdailey commented on pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "jdailey (via GitHub)" <gi...@apache.org>.
jdailey commented on PR #3192:
URL: https://github.com/apache/fineract/pull/3192#issuecomment-1587596052

   Let's figure out a solution to the auto checks failing.  Gradle upgrade could be part of this.  


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1201573158


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java:
##########
@@ -132,6 +132,22 @@ public void reverse() {
         this.reversed = true;
     }
 
+    public LocalDate getDate() {

Review Comment:
   Why not Lombok @Getter?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {

Review Comment:
   DTOV2? Where's DTOV1? Can't we get a better name for this that represents the use-case?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {
+
+    private final Long transactionId;
+    private final Integer transactionType;
+    private final LocalDate transactionDate;
+    private final BigDecimal transactionAmount;
+    private final Long releaseIdOfHoldAmountTransaction;
+    private final String reasonForBlock;
+    private final LocalDateTime createdDate;
+    private final AppUser appUser;
+    private final String note;
+    private final BigDecimal runningBalance;
+    private final boolean reversed;
+    private final boolean reversalTransaction;
+    private final Long originalTxnId;
+    private final Boolean lienTransaction;
+    private final boolean isManualTransaction;
+    private final AccountTransferTransaction fromSavingsTransaction;
+    private final AccountTransferTransaction toSavingsTransaction;
+    private final SavingsAccount savingsAccount;
+    private final PaymentDetail paymentDetail;
+    private final ApplicationCurrency currency;
+
+    public static final SavingsAccountTransactionData tosavingsAccountTransactionData(SavingsAccountTransactionDTOV2 dto) {

Review Comment:
   Not conforming the Java naming convention (read: not camelCase)



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {
+
+    private final Long transactionId;
+    private final Integer transactionType;
+    private final LocalDate transactionDate;
+    private final BigDecimal transactionAmount;
+    private final Long releaseIdOfHoldAmountTransaction;
+    private final String reasonForBlock;
+    private final LocalDateTime createdDate;
+    private final AppUser appUser;
+    private final String note;
+    private final BigDecimal runningBalance;
+    private final boolean reversed;
+    private final boolean reversalTransaction;
+    private final Long originalTxnId;
+    private final Boolean lienTransaction;
+    private final boolean isManualTransaction;
+    private final AccountTransferTransaction fromSavingsTransaction;
+    private final AccountTransferTransaction toSavingsTransaction;
+    private final SavingsAccount savingsAccount;
+    private final PaymentDetail paymentDetail;
+    private final ApplicationCurrency currency;
+
+    public static final SavingsAccountTransactionData tosavingsAccountTransactionData(SavingsAccountTransactionDTOV2 dto) {

Review Comment:
   Are you planning to create a unit test for this class? Seems like there's way too much logic here than it should be.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java:
##########
@@ -53,4 +56,28 @@ List<SavingsAccountTransaction> findTransactionRunningBalanceBeforePivotDate(@Pa
     @Query("select sat from SavingsAccountTransaction sat where sat.savingsAccount.id = :savingsId and sat.dateOf <= :transactionDate and sat.reversed=false")
     List<SavingsAccountTransaction> findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(@Param("savingsId") Long savingsId,
             @Param("transactionDate") LocalDate transactionDate, Pageable pageable);
+
+    @Query("""
+                    SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTOV2(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
+                )
+                    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
+                    AND (:transactionType IS NULL OR tr.typeOf = :transactionType )
+                    AND (:#{#searchParameters.fromDate} IS NULL OR tr.dateOf >= :#{#searchParameters.fromDate} )
+                    AND (:#{#searchParameters.toDate} IS NULL OR tr.dateOf <= :#{#searchParameters.toDate} )
+                    AND (:#{#searchParameters.fromAmount} IS NULL OR tr.amount >= :#{#searchParameters.fromAmount} )
+                    AND (:#{#searchParameters.toAmount} IS NULL OR tr.amount <= :#{#searchParameters.toAmount} )
+            """)
+    Page<SavingsAccountTransactionDTOV2> findAll(@Param("savingsId") Long savingsId, @Param("depositType") Integer depositType,

Review Comment:
   findAll method naming is confusing cause it's not finding all and this is not an entity either. This use-case is for search, name it accordingly.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -1267,6 +1283,136 @@ public Collection<SavingsAccountTransactionData> retrieveAllTransactions(final L
         return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR
     }
 
+    @Override
+    public Page<SavingsAccountTransactionData> retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType,
+            SearchParameters searchParameters) {
+        // validate the search parameters
+        validateSearchParameters(searchParameters);
+
+        Integer page = 0;
+        Integer size = SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTIONS_DEFAULT_LIMIT;
+        if (searchParameters.isLimited() && searchParameters.isOffset()) {
+            Integer limit = searchParameters.getLimit();
+            page = searchParameters.getOffset() / limit;
+            size = limit;
+        }
+        Pageable pageable = PageRequest.of(page, size);

Review Comment:
   What's the point of creating this PageRequest object if you override it in the following if/else?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/domain/PaymentDetail.java:
##########
@@ -121,4 +121,16 @@ public String getReceiptNumber() {
     public String getRoutingCode() {
         return routingCode;
     }
+
+    public String getAccountNumber() {

Review Comment:
   Why not Lombok @Getter?



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {

Review Comment:
   Don't see a test-case for Asc ordering.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        queryParametersMap.put("sortOrder", "DESC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();

Review Comment:
   I don't like this logic here. Why can't we simply check the 1., 2., 3. items in the list? If the ordering works, it should be in the proper order in the list as well.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        queryParametersMap.put("sortOrder", "DESC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isDescending = isListOrdered(actualList, "DESC");
+
+        assertEquals(true, isDescending);
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDefault() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isAscending = isListOrdered(actualList, null);
+        // Default Sort is ASC if sortOrder is not provided
+        assertEquals(true, isAscending);
+
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByTransactionDateAsc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "dateOf");
+        queryParametersMap.put("sortOrder", "ASC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> actualList = new ArrayList<>();

Review Comment:
   Same as above.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        queryParametersMap.put("sortOrder", "DESC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isDescending = isListOrdered(actualList, "DESC");
+
+        assertEquals(true, isDescending);
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDefault() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isAscending = isListOrdered(actualList, null);
+        // Default Sort is ASC if sortOrder is not provided
+        assertEquals(true, isAscending);
+
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByTransactionDateAsc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "dateOf");
+        queryParametersMap.put("sortOrder", "ASC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getDate());
+        });
+        boolean isAscending = isDateListOrdered(actualList, "ASC");
+        assertEquals(true, isAscending);
+
+    }
+
+    @Test
+    public void testSavingsTransactionsFilterByTransactionType() {

Review Comment:
   This test is just half of the pie because you never tested whether without the filtering applied, the post interest transaction is also returned; i.e. you don't know if your filter parameter that you passed has been handled or it's because by default the API filters out non deposit transactions.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        queryParametersMap.put("sortOrder", "DESC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isDescending = isListOrdered(actualList, "DESC");
+
+        assertEquals(true, isDescending);
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDefault() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isAscending = isListOrdered(actualList, null);
+        // Default Sort is ASC if sortOrder is not provided
+        assertEquals(true, isAscending);
+
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByTransactionDateAsc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "dateOf");
+        queryParametersMap.put("sortOrder", "ASC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<LocalDate> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getDate());
+        });
+        boolean isAscending = isDateListOrdered(actualList, "ASC");
+        assertEquals(true, isAscending);
+
+    }
+
+    @Test
+    public void testSavingsTransactionsFilterByTransactionType() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final String withdrawDate = "09 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("transactionType", "deposit");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.postInterestForSavings(savingsId);
+
+        this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "400", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        for (GetSavingsAccountTransactionsPageItem item : pageItems) {
+            assertEquals(true, item.getTransactionType().getDeposit(), "Transaction Type is not as expected");
+        }
+    }
+
+    @Test
+    public void testSavingsTransactionsFilterByTransactionTypeAndAmount() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final String withdrawDate = "09 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("transactionType", "deposit");
+        queryParametersMap.put("fromAmount", "400");
+        queryParametersMap.put("toAmount", "700");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.postInterestForSavings(savingsId);
+
+        this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "400", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(2, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        for (GetSavingsAccountTransactionsPageItem item : pageItems) {
+            assertEquals(true, item.getTransactionType().getDeposit(), "Transaction Type is not as expected");
+            assertEquals(true, item.getAmount().compareTo(BigDecimal.valueOf(700)) <= 0);

Review Comment:
   if you're checking this, why don't you check for 400?



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -1267,6 +1283,136 @@ public Collection<SavingsAccountTransactionData> retrieveAllTransactions(final L
         return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR
     }
 
+    @Override
+    public Page<SavingsAccountTransactionData> retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType,
+            SearchParameters searchParameters) {
+        // validate the search parameters
+        validateSearchParameters(searchParameters);
+
+        Integer page = 0;
+        Integer size = SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTIONS_DEFAULT_LIMIT;
+        if (searchParameters.isLimited() && searchParameters.isOffset()) {
+            Integer limit = searchParameters.getLimit();
+            page = searchParameters.getOffset() / limit;
+            size = limit;
+        }
+        Pageable pageable = PageRequest.of(page, size);
+        if (searchParameters.isOrderByRequested()) {
+            pageable = PageRequest.of(page, size, Sort.by(searchParameters.getOrderBy()));
+            if (searchParameters.isSortOrderProvided()) {
+
+                pageable = PageRequest.of(page, size,
+                        Sort.by(searchParameters.getSortOrder().equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.Direction.ASC
+                                : Sort.Direction.DESC, searchParameters.getOrderBy()));
+            }
+        } else {
+            List<Order> orders = new ArrayList<Order>();
+            Order order1 = new Order(Sort.Direction.DESC, "dateOf");
+            orders.add(order1);
+
+            Order order2 = new Order(Sort.Direction.DESC, "createdDate");
+            orders.add(order2);
+
+            Order order3 = new Order(Sort.Direction.DESC, "id");
+            orders.add(order3);
+            pageable = PageRequest.of(page, size, Sort.by(orders));
+        }
+        Integer transactionTypeEnum = null;
+        if (searchParameters.isTransactionTypeProvided()) {
+            transactionTypeEnum = SavingsAccountTransactionType.fromString(searchParameters.getTransactionType()).getValue();
+        }
+        org.springframework.data.domain.Page<SavingsAccountTransactionDTOV2> resultPage = savingsAccountTransactionsRepository
+                .findAll(savingsId, depositAccountType.getValue(), transactionTypeEnum, searchParameters, pageable);
+        List<SavingsAccountTransactionData> savingsAccountTransactionDataList = resultPage.stream()
+                .map(SavingsAccountTransactionDTOV2::tosavingsAccountTransactionData).collect(Collectors.toList());
+        return new Page<>(savingsAccountTransactionDataList, Long.valueOf(resultPage.getTotalElements()).intValue());
+    }
+
+    private void validateSearchParameters(final SearchParameters searchParameters) {
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        if (searchParameters.isFromDateProvided() && searchParameters.isToDateProvided()) {
+            if (searchParameters.getToDate().isBefore(searchParameters.getFromDate())) {
+                final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                        .append(SavingsApiConstants.toDateParamName).append(".can.not.be.before").append(".")
+                        .append(SavingsApiConstants.fromDateParamName);
+                final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toDateParamName)
+                        .append("` must be less than the provided ").append(SavingsApiConstants.fromDateParamName);
+                final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                        defaultEnglishMessage.toString(), SavingsApiConstants.toDateParamName,
+                        searchParameters.getToDate().format(DateTimeFormatter.ISO_LOCAL_DATE),
+                        searchParameters.getFromDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
+                dataValidationErrors.add(error);
+            }
+        }
+
+        if (searchParameters.isTransactionTypeProvided() && !isValidTransactionType(searchParameters.getTransactionType())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.transactionTypeParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `")
+                    .append(SavingsApiConstants.transactionTypeParamName).append("` provided is not a valid transaction type ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.transactionTypeParamName, searchParameters.getTransactionType());
+            dataValidationErrors.add(error);
+        }
+
+        if (searchParameters.isFromAmountProvided() && searchParameters.isToAmountProvided()
+                && !isValidAmount(searchParameters.getFromAmount(), searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".or").append(SavingsApiConstants.toAmountParamName)
+                    .append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` should be less than ").append(SavingsApiConstants.toAmountParamName);
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isFromAmountProvided() && !isValidAmount(searchParameters.getFromAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isToAmountProvided() && !isValidAmount(searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.toAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.toAmountParamName, searchParameters.getToAmount());
+            dataValidationErrors.add(error);
+        }
+
+        if (!dataValidationErrors.isEmpty()) {
+            throw new PlatformApiDataValidationException(dataValidationErrors);
+        }
+    }
+
+    private boolean isValidAmount(BigDecimal amount) {

Review Comment:
   I bet fineract already has code for this, please check.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -126,7 +141,7 @@ public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte
             final ChargeReadPlatformService chargeReadPlatformService,
             final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator,
             final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper,
-            DatabaseSpecificSQLGenerator sqlGenerator) {
+            DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountTransactionRepository savingsAccountTransactionsRepository) {

Review Comment:
   Could've replced this with a lombok annotation.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {
+
+    private final Long transactionId;
+    private final Integer transactionType;
+    private final LocalDate transactionDate;
+    private final BigDecimal transactionAmount;
+    private final Long releaseIdOfHoldAmountTransaction;
+    private final String reasonForBlock;
+    private final LocalDateTime createdDate;
+    private final AppUser appUser;
+    private final String note;
+    private final BigDecimal runningBalance;
+    private final boolean reversed;
+    private final boolean reversalTransaction;
+    private final Long originalTxnId;
+    private final Boolean lienTransaction;
+    private final boolean isManualTransaction;
+    private final AccountTransferTransaction fromSavingsTransaction;
+    private final AccountTransferTransaction toSavingsTransaction;
+    private final SavingsAccount savingsAccount;
+    private final PaymentDetail paymentDetail;
+    private final ApplicationCurrency currency;
+
+    public static final SavingsAccountTransactionData tosavingsAccountTransactionData(SavingsAccountTransactionDTOV2 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().toLocalDate()).orElse(null);
+        final BigDecimal amount = dto.getTransactionAmount();
+        final Long releaseTransactionId = Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(0L);

Review Comment:
   Is this right? If there's no release transaction, we simply return a 0 as Id? This is misleading in my opinion to clients unless you explicitly mark and document this in the swagger docs.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {
+
+    private final Long transactionId;
+    private final Integer transactionType;
+    private final LocalDate transactionDate;
+    private final BigDecimal transactionAmount;
+    private final Long releaseIdOfHoldAmountTransaction;
+    private final String reasonForBlock;
+    private final LocalDateTime createdDate;
+    private final AppUser appUser;
+    private final String note;
+    private final BigDecimal runningBalance;
+    private final boolean reversed;
+    private final boolean reversalTransaction;
+    private final Long originalTxnId;
+    private final Boolean lienTransaction;
+    private final boolean isManualTransaction;
+    private final AccountTransferTransaction fromSavingsTransaction;
+    private final AccountTransferTransaction toSavingsTransaction;
+    private final SavingsAccount savingsAccount;
+    private final PaymentDetail paymentDetail;
+    private final ApplicationCurrency currency;
+
+    public static final SavingsAccountTransactionData tosavingsAccountTransactionData(SavingsAccountTransactionDTOV2 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().toLocalDate()).orElse(null);
+        final BigDecimal amount = dto.getTransactionAmount();
+        final Long releaseTransactionId = Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(0L);
+        final String reasonForBlock = dto.getReasonForBlock();
+        final BigDecimal outstandingChargeAmount = null;
+        final BigDecimal runningBalance = dto.getRunningBalance();
+        final boolean reversed = dto.isReversed();
+        final boolean isReversal = dto.isReversalTransaction();
+        final Long originalTransactionId = Optional.ofNullable(dto.getOriginalTxnId()).orElse(0L);

Review Comment:
   Same as above.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTOV2.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 SavingsAccountTransactionDTOV2 {
+
+    private final Long transactionId;
+    private final Integer transactionType;
+    private final LocalDate transactionDate;
+    private final BigDecimal transactionAmount;
+    private final Long releaseIdOfHoldAmountTransaction;
+    private final String reasonForBlock;
+    private final LocalDateTime createdDate;
+    private final AppUser appUser;
+    private final String note;
+    private final BigDecimal runningBalance;
+    private final boolean reversed;
+    private final boolean reversalTransaction;
+    private final Long originalTxnId;
+    private final Boolean lienTransaction;
+    private final boolean isManualTransaction;
+    private final AccountTransferTransaction fromSavingsTransaction;
+    private final AccountTransferTransaction toSavingsTransaction;
+    private final SavingsAccount savingsAccount;
+    private final PaymentDetail paymentDetail;
+    private final ApplicationCurrency currency;
+
+    public static final SavingsAccountTransactionData tosavingsAccountTransactionData(SavingsAccountTransactionDTOV2 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().toLocalDate()).orElse(null);
+        final BigDecimal amount = dto.getTransactionAmount();
+        final Long releaseTransactionId = Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(0L);
+        final String reasonForBlock = dto.getReasonForBlock();
+        final BigDecimal outstandingChargeAmount = null;
+        final BigDecimal runningBalance = dto.getRunningBalance();
+        final boolean reversed = dto.isReversed();
+        final boolean isReversal = dto.isReversalTransaction();
+        final Long originalTransactionId = Optional.ofNullable(dto.getOriginalTxnId()).orElse(0L);
+        final Boolean lienTransaction = dto.getLienTransaction();
+
+        final Long savingsId = dto.getSavingsAccount().getId();
+        final String accountNo = dto.getSavingsAccount().getAccountNumber();
+        final boolean postInterestAsOn = dto.isManualTransaction;
+
+        PaymentDetailData paymentDetailData = null;
+        if (transactionType.isDepositOrWithdrawal()) {
+            final Long paymentTypeId = dto.getPaymentDetail().getPaymentType().getId();
+            if (paymentTypeId != null) {
+                final String typeName = dto.getPaymentDetail().getPaymentType().getName();
+                final PaymentTypeData paymentType = PaymentTypeData.instance(paymentTypeId, typeName);
+                final String accountNumber = dto.getPaymentDetail().getAccountNumber();
+                final String checkNumber = dto.getPaymentDetail().getCheckNumber();
+                final String routingCode = dto.getPaymentDetail().getRoutingCode();
+                final String receiptNumber = dto.getPaymentDetail().getReceiptNumber();
+                final String bankNumber = dto.getPaymentDetail().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 = getOrDefault(transferFrom.getAmount(), 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 = getOrDefault(transferTo.getAmount(), BigDecimal.ZERO);
+            final boolean toTransferReversed = transferTo.isReversed();
+            final String toTransferDescription = transferTo.getDescription();
+
+            transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate,
+                    toTransferDescription, toTransferReversed);
+        }
+        String submittedByUsername = null;
+        if (Objects.nonNull(dto.getAppUser())) {
+            submittedByUsername = getOrDefault(dto.getAppUser().getUsername(), null);
+        }
+        final String note = dto.getNote();
+
+        return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, amount,
+                outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, submittedByUsername, note,
+                isReversal, originalTransactionId, lienTransaction, releaseTransactionId, reasonForBlock);
+    }
+
+    private static <T> T getOrDefault(T input, T defaultValue) {

Review Comment:
   Single responsibility. How come the SavingsAccountTransaction has to deal with getOrDefault logic? You can use Objects.requireNonNull(T object, T defaultObject) instead of this..



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java:
##########
@@ -124,6 +127,20 @@ public static SavingsAccountTransactionType fromInt(final Integer transactionTyp
         return savingsAccountTransactionType;
     }
 
+    public static SavingsAccountTransactionType fromString(String transactionType) {
+        for (SavingsAccountTransactionType savingsAccountTransactionType : SavingsAccountTransactionType.values()) {

Review Comment:
   I really really don't like this splitting logic. Why can't we solve it differently which doesn't involve string magic? this solution is super fragile.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java:
##########
@@ -646,6 +649,27 @@ public HashMap getSavingsTransaction(final Integer savingsID, final Integer savi
         return Utils.performServerGet(requestSpec, responseSpec, URL, "");
     }
 
+    public GetSavingsAccountTransactionsResponse getSavingsTransactionsV2(final Integer savingsID) {
+        final String URL = SAVINGS_ACCOUNT_URL + "/" + savingsID + "/transactions" + "?" + Utils.TENANT_IDENTIFIER;
+        final String response = Utils.performServerGet(requestSpec, responseSpec, URL, null);
+        return GSON.fromJson(response, GetSavingsAccountTransactionsResponse.class);
+    }
+
+    public GetSavingsAccountTransactionsResponse getSavingsTransactionsWithQueryParams(final Integer savingsID,
+            Map<String, String> queryParamMap) {
+        final String URL = SAVINGS_ACCOUNT_URL + "/" + savingsID + "/transactions" + "?" + Utils.TENANT_IDENTIFIER;
+        StringBuilder builder = new StringBuilder(URL);
+
+        for (Map.Entry<String, String> entry : queryParamMap.entrySet()) {

Review Comment:
   Why don't we use the generated fineract-client classes? That way you don't even need to do this string magic again on the query params.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());

Review Comment:
   The details of the savings transaction is not checked (amount, etc)



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -1267,6 +1283,136 @@ public Collection<SavingsAccountTransactionData> retrieveAllTransactions(final L
         return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR
     }
 
+    @Override
+    public Page<SavingsAccountTransactionData> retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType,
+            SearchParameters searchParameters) {
+        // validate the search parameters
+        validateSearchParameters(searchParameters);
+
+        Integer page = 0;
+        Integer size = SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTIONS_DEFAULT_LIMIT;
+        if (searchParameters.isLimited() && searchParameters.isOffset()) {
+            Integer limit = searchParameters.getLimit();
+            page = searchParameters.getOffset() / limit;
+            size = limit;
+        }
+        Pageable pageable = PageRequest.of(page, size);
+        if (searchParameters.isOrderByRequested()) {
+            pageable = PageRequest.of(page, size, Sort.by(searchParameters.getOrderBy()));
+            if (searchParameters.isSortOrderProvided()) {
+
+                pageable = PageRequest.of(page, size,
+                        Sort.by(searchParameters.getSortOrder().equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.Direction.ASC
+                                : Sort.Direction.DESC, searchParameters.getOrderBy()));
+            }
+        } else {
+            List<Order> orders = new ArrayList<Order>();
+            Order order1 = new Order(Sort.Direction.DESC, "dateOf");
+            orders.add(order1);
+
+            Order order2 = new Order(Sort.Direction.DESC, "createdDate");
+            orders.add(order2);
+
+            Order order3 = new Order(Sort.Direction.DESC, "id");
+            orders.add(order3);
+            pageable = PageRequest.of(page, size, Sort.by(orders));
+        }
+        Integer transactionTypeEnum = null;
+        if (searchParameters.isTransactionTypeProvided()) {
+            transactionTypeEnum = SavingsAccountTransactionType.fromString(searchParameters.getTransactionType()).getValue();
+        }
+        org.springframework.data.domain.Page<SavingsAccountTransactionDTOV2> resultPage = savingsAccountTransactionsRepository
+                .findAll(savingsId, depositAccountType.getValue(), transactionTypeEnum, searchParameters, pageable);
+        List<SavingsAccountTransactionData> savingsAccountTransactionDataList = resultPage.stream()
+                .map(SavingsAccountTransactionDTOV2::tosavingsAccountTransactionData).collect(Collectors.toList());
+        return new Page<>(savingsAccountTransactionDataList, Long.valueOf(resultPage.getTotalElements()).intValue());
+    }
+
+    private void validateSearchParameters(final SearchParameters searchParameters) {
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        if (searchParameters.isFromDateProvided() && searchParameters.isToDateProvided()) {
+            if (searchParameters.getToDate().isBefore(searchParameters.getFromDate())) {
+                final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                        .append(SavingsApiConstants.toDateParamName).append(".can.not.be.before").append(".")
+                        .append(SavingsApiConstants.fromDateParamName);
+                final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toDateParamName)
+                        .append("` must be less than the provided ").append(SavingsApiConstants.fromDateParamName);
+                final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                        defaultEnglishMessage.toString(), SavingsApiConstants.toDateParamName,
+                        searchParameters.getToDate().format(DateTimeFormatter.ISO_LOCAL_DATE),
+                        searchParameters.getFromDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
+                dataValidationErrors.add(error);
+            }
+        }
+
+        if (searchParameters.isTransactionTypeProvided() && !isValidTransactionType(searchParameters.getTransactionType())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.transactionTypeParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `")
+                    .append(SavingsApiConstants.transactionTypeParamName).append("` provided is not a valid transaction type ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.transactionTypeParamName, searchParameters.getTransactionType());
+            dataValidationErrors.add(error);
+        }
+
+        if (searchParameters.isFromAmountProvided() && searchParameters.isToAmountProvided()
+                && !isValidAmount(searchParameters.getFromAmount(), searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".or").append(SavingsApiConstants.toAmountParamName)
+                    .append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` should be less than ").append(SavingsApiConstants.toAmountParamName);
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isFromAmountProvided() && !isValidAmount(searchParameters.getFromAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isToAmountProvided() && !isValidAmount(searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.toAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.toAmountParamName, searchParameters.getToAmount());
+            dataValidationErrors.add(error);
+        }
+
+        if (!dataValidationErrors.isEmpty()) {
+            throw new PlatformApiDataValidationException(dataValidationErrors);
+        }
+    }
+
+    private boolean isValidAmount(BigDecimal amount) {
+        BigDecimal zero = BigDecimal.ZERO;
+        BigDecimal maxValue = BigDecimal.valueOf(Double.MAX_VALUE);
+
+        return (amount.compareTo(zero) >= 0) && (amount.compareTo(maxValue) <= 0);
+    }
+
+    private boolean isValidAmount(BigDecimal fromAmount, BigDecimal toAmount) {

Review Comment:
   I bet fineract already has code for this, please check.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -1267,6 +1283,136 @@ public Collection<SavingsAccountTransactionData> retrieveAllTransactions(final L
         return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR
     }
 
+    @Override
+    public Page<SavingsAccountTransactionData> retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType,
+            SearchParameters searchParameters) {
+        // validate the search parameters
+        validateSearchParameters(searchParameters);
+
+        Integer page = 0;
+        Integer size = SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTIONS_DEFAULT_LIMIT;
+        if (searchParameters.isLimited() && searchParameters.isOffset()) {
+            Integer limit = searchParameters.getLimit();
+            page = searchParameters.getOffset() / limit;
+            size = limit;
+        }
+        Pageable pageable = PageRequest.of(page, size);
+        if (searchParameters.isOrderByRequested()) {
+            pageable = PageRequest.of(page, size, Sort.by(searchParameters.getOrderBy()));
+            if (searchParameters.isSortOrderProvided()) {
+
+                pageable = PageRequest.of(page, size,
+                        Sort.by(searchParameters.getSortOrder().equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.Direction.ASC
+                                : Sort.Direction.DESC, searchParameters.getOrderBy()));
+            }
+        } else {
+            List<Order> orders = new ArrayList<Order>();
+            Order order1 = new Order(Sort.Direction.DESC, "dateOf");
+            orders.add(order1);
+
+            Order order2 = new Order(Sort.Direction.DESC, "createdDate");
+            orders.add(order2);
+
+            Order order3 = new Order(Sort.Direction.DESC, "id");
+            orders.add(order3);
+            pageable = PageRequest.of(page, size, Sort.by(orders));
+        }
+        Integer transactionTypeEnum = null;
+        if (searchParameters.isTransactionTypeProvided()) {
+            transactionTypeEnum = SavingsAccountTransactionType.fromString(searchParameters.getTransactionType()).getValue();
+        }
+        org.springframework.data.domain.Page<SavingsAccountTransactionDTOV2> resultPage = savingsAccountTransactionsRepository
+                .findAll(savingsId, depositAccountType.getValue(), transactionTypeEnum, searchParameters, pageable);
+        List<SavingsAccountTransactionData> savingsAccountTransactionDataList = resultPage.stream()
+                .map(SavingsAccountTransactionDTOV2::tosavingsAccountTransactionData).collect(Collectors.toList());
+        return new Page<>(savingsAccountTransactionDataList, Long.valueOf(resultPage.getTotalElements()).intValue());
+    }
+
+    private void validateSearchParameters(final SearchParameters searchParameters) {
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        if (searchParameters.isFromDateProvided() && searchParameters.isToDateProvided()) {
+            if (searchParameters.getToDate().isBefore(searchParameters.getFromDate())) {
+                final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                        .append(SavingsApiConstants.toDateParamName).append(".can.not.be.before").append(".")
+                        .append(SavingsApiConstants.fromDateParamName);
+                final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toDateParamName)
+                        .append("` must be less than the provided ").append(SavingsApiConstants.fromDateParamName);
+                final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                        defaultEnglishMessage.toString(), SavingsApiConstants.toDateParamName,
+                        searchParameters.getToDate().format(DateTimeFormatter.ISO_LOCAL_DATE),
+                        searchParameters.getFromDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
+                dataValidationErrors.add(error);
+            }
+        }
+
+        if (searchParameters.isTransactionTypeProvided() && !isValidTransactionType(searchParameters.getTransactionType())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.transactionTypeParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `")
+                    .append(SavingsApiConstants.transactionTypeParamName).append("` provided is not a valid transaction type ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.transactionTypeParamName, searchParameters.getTransactionType());
+            dataValidationErrors.add(error);
+        }
+
+        if (searchParameters.isFromAmountProvided() && searchParameters.isToAmountProvided()
+                && !isValidAmount(searchParameters.getFromAmount(), searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".or").append(SavingsApiConstants.toAmountParamName)
+                    .append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` should be less than ").append(SavingsApiConstants.toAmountParamName);
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isFromAmountProvided() && !isValidAmount(searchParameters.getFromAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.fromAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.fromAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.fromAmountParamName, searchParameters.getFromAmount());
+            dataValidationErrors.add(error);
+        } else if (searchParameters.isToAmountProvided() && !isValidAmount(searchParameters.getToAmount())) {
+            final StringBuilder validationErrorCode = new StringBuilder("validation.msg").append(".")
+                    .append(SavingsApiConstants.toAmountParamName).append(".invalid");
+            final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(SavingsApiConstants.toAmountParamName)
+                    .append("` provided is not a valid amount ");
+            final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
+                    defaultEnglishMessage.toString(), SavingsApiConstants.toAmountParamName, searchParameters.getToAmount());
+            dataValidationErrors.add(error);
+        }
+
+        if (!dataValidationErrors.isEmpty()) {
+            throw new PlatformApiDataValidationException(dataValidationErrors);
+        }
+    }
+
+    private boolean isValidAmount(BigDecimal amount) {
+        BigDecimal zero = BigDecimal.ZERO;
+        BigDecimal maxValue = BigDecimal.valueOf(Double.MAX_VALUE);
+
+        return (amount.compareTo(zero) >= 0) && (amount.compareTo(maxValue) <= 0);
+    }
+
+    private boolean isValidAmount(BigDecimal fromAmount, BigDecimal toAmount) {
+        BigDecimal zero = BigDecimal.ZERO;
+        BigDecimal maxValue = BigDecimal.valueOf(Double.MAX_VALUE);
+
+        boolean isValidFromAmount = isValidAmount(fromAmount);
+        boolean isValidToAmount = isValidAmount(toAmount);
+        boolean isValidFromToAmount = fromAmount.compareTo(toAmount) <= 0;
+
+        return isValidFromAmount && isValidToAmount && isValidFromToAmount;
+    }
+
+    private boolean isValidTransactionType(String transactionType) {

Review Comment:
   I bet fineract already has code for this, please check.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);

Review Comment:
   Please import this statically to have a consistent structure.



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java:
##########
@@ -45,6 +45,9 @@ public interface SavingsAccountReadPlatformService {
 
     Collection<SavingsAccountTransactionData> retrieveAllTransactions(Long savingsId, DepositAccountType depositAccountType);
 
+    Page<SavingsAccountTransactionData> retrieveAllTransactions(Long savingsId, DepositAccountType depositAccountType,

Review Comment:
   Same as I said for the repository, search use-case.



##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        queryParametersMap.put("sortOrder", "DESC");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();
+        pageItems.forEach(data -> {
+            actualList.add(data.getAmount());
+        });
+        boolean isDescending = isListOrdered(actualList, "DESC");
+
+        assertEquals(true, isDescending);
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDefault() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("orderBy", "amount");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(3, transactionsResponse.getPageItems().size());
+        Set<GetSavingsAccountTransactionsPageItem> pageItems = transactionsResponse.getPageItems();
+        List<BigDecimal> actualList = new ArrayList<>();

Review Comment:
   Same as above.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] adamsaghy commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "adamsaghy (via GitHub)" <gi...@apache.org>.
adamsaghy commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217786385


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   @Mk9894 I see.. would you please change than only to be this v1?
   
   The idea is v2 is using the new way of generating the swagger definitions and using the Spring data page and some other enhancements, it this is to be support the legacy pagination, it should remain v1.
   
   is that okay?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] adamsaghy commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "adamsaghy (via GitHub)" <gi...@apache.org>.
adamsaghy commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217786385


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   @Mk9894 I see.. would you please change than only to be this v1?
   
   The idea is v2 is using the new way of generating the swagger definitions and using the Spring data page and some other enhancements, if this is to support the legacy pagination, it should remain v1.
   
   is that okay?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1226027393


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java:
##########
@@ -0,0 +1,28 @@
+/**
+ * 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.PagedRequest;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+
+public interface SavingsAccountTransactionSearchService {

Review Comment:
   Implemented the interface



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1214731701


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java:
##########
@@ -0,0 +1,152 @@
+/**
+ * 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.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+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.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData;
+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 SavingsAccountTransactionSearchData toSavingsAccountTransactionSearchData(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(0L);

Review Comment:
   @galovics I can return a null value if getReleaseIdOfHoldAmountTransaction is null . But The existing implementation of SavingsAccountTransactions returns a 0 value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206650687


##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {

Review Comment:
   That's not enough. There are 3 use-cases for sorting:
   - default
   - asc
   - desc
   
   From which asc is not covered. Imagine the default changing from asc to desc sorting. Then you won't have any proof that asc sorting works.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206652373


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java:
##########
@@ -53,4 +56,28 @@ List<SavingsAccountTransaction> findTransactionRunningBalanceBeforePivotDate(@Pa
     @Query("select sat from SavingsAccountTransaction sat where sat.savingsAccount.id = :savingsId and sat.dateOf <= :transactionDate and sat.reversed=false")
     List<SavingsAccountTransaction> findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(@Param("savingsId") Long savingsId,
             @Param("transactionDate") LocalDate transactionDate, Pageable pageable);
+
+    @Query("""
+                    SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTOV2(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
+                )
+                    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
+                    AND (:transactionType IS NULL OR tr.typeOf = :transactionType )
+                    AND (:#{#searchParameters.fromDate} IS NULL OR tr.dateOf >= :#{#searchParameters.fromDate} )
+                    AND (:#{#searchParameters.toDate} IS NULL OR tr.dateOf <= :#{#searchParameters.toDate} )
+                    AND (:#{#searchParameters.fromAmount} IS NULL OR tr.amount >= :#{#searchParameters.fromAmount} )
+                    AND (:#{#searchParameters.toAmount} IS NULL OR tr.amount <= :#{#searchParameters.toAmount} )
+            """)
+    Page<SavingsAccountTransactionDTOV2> findAll(@Param("savingsId") Long savingsId, @Param("depositType") Integer depositType,

Review Comment:
   Still I think this is a search use-case and should be named accordingly.
   
   A POST API is also fine with me, but if you're planning to create a new POST API, then please take a look at ClientSearchV2ApiResource cause it could act as a base pattern for the implementation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] adamsaghy commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "adamsaghy (via GitHub)" <gi...@apache.org>.
adamsaghy commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217700629


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   Please use the Spring DATA Page instead of the "own"... 
   The idea is to move towards the standard solutions and let the "custom" ones got deprecated slowly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1201604470


##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsIntegrationTest.java:
##########
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem;
+import org.apache.fineract.client.models.GetSavingsAccountTransactionsResponse;
+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 SavingsAccountTransactionsIntegrationTest {
+
+    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private SavingsProductHelper savingsProductHelper;
+    private SavingsAccountHelper savingsAccountHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+        this.savingsProductHelper = new SavingsProductHelper();
+    }
+
+    @Test
+    public void testSavingsTransactions() {
+        final String startDate = "01 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
+        Assertions.assertNotNull(clientID);
+
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper.getSavingsTransactionsV2(savingsId);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(1, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+    }
+
+    @Test
+    public void testSavingsTransactionsPagination() {
+        final String clientCreateDate = "25 Apr 2023";
+        final String startDate1 = "01 May 2023";
+        final String startDate2 = "04 May 2023";
+        final String startDate3 = "07 May 2023";
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, clientCreateDate);
+        Assertions.assertNotNull(clientID);
+        Map<String, String> queryParametersMap = new HashMap<>();
+        queryParametersMap.put("offset", "0");
+        queryParametersMap.put("limit", "2");
+        final Integer savingsId = createSavingsAccountDailyPosting(clientID, clientCreateDate);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "1000", startDate1, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "500", startDate2, CommonConstants.RESPONSE_RESOURCE_ID);
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, "700", startDate3, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        GetSavingsAccountTransactionsResponse transactionsResponse = this.savingsAccountHelper
+                .getSavingsTransactionsWithQueryParams(savingsId, queryParametersMap);
+        Assertions.assertNotNull(transactionsResponse);
+        assertEquals(3, transactionsResponse.getTotalFilteredRecords());
+        Assertions.assertNotNull(transactionsResponse.getPageItems());
+        assertEquals(2, transactionsResponse.getPageItems().size());
+    }
+
+    @Test
+    public void testSavingsTransactionsSortByAmountDesc() {

Review Comment:
   @galovics There is a test case for Default sorting. The default sorting is ASC.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1206537543


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java:
##########
@@ -126,7 +141,7 @@ public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte
             final ChargeReadPlatformService chargeReadPlatformService,
             final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator,
             final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper,
-            DatabaseSpecificSQLGenerator sqlGenerator) {
+            DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountTransactionRepository savingsAccountTransactionsRepository) {

Review Comment:
   Yes. But I am afraid if it would break the existing implementation



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] chtompki commented on pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "chtompki (via GitHub)" <gi...@apache.org>.
chtompki commented on PR #3192:
URL: https://github.com/apache/fineract/pull/3192#issuecomment-1574977920

   I'm going to read in on this PR to see if I can help out with the lift.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217765796


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   Hi Adam,
   
   I had also considered utilizing the Spring Data Page instead of the legacy response. However, Peter expressed a preference for the response to resemble the legacy implementation. If it is necessary for me to accommodate the legacy response fields while incorporating the spring data Page, it will require a refactoring of all the transaction-related entities.
   I am concerned that making those changes may potentially disrupt the existing implementations.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1226027037


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java:
##########
@@ -0,0 +1,80 @@
+/**
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Data;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+@Data
+public class SavingsTransactionSearch {

Review Comment:
   Done



##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java:
##########
@@ -0,0 +1,80 @@
+/**
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.Data;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+@Data
+public class SavingsTransactionSearch {

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] Mk9894 commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "Mk9894 (via GitHub)" <gi...@apache.org>.
Mk9894 commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1226026443


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java:
##########
@@ -118,6 +124,19 @@ public String retrieveOne(@PathParam("savingsId") final Long savingsId, @PathPar
                 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,

Review Comment:
   This is done to match the legacy response of savings account transactions



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] adamsaghy commented on a diff in pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "adamsaghy (via GitHub)" <gi...@apache.org>.
adamsaghy commented on code in PR #3192:
URL: https://github.com/apache/fineract/pull/3192#discussion_r1217786385


##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/v2/search/SavingsAccountTransactionsSearchApiResource.java:
##########
@@ -0,0 +1,66 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+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.portfolio.savings.api.SavingsAccountTransactionsApiResourceSwagger;
+import org.apache.fineract.portfolio.savings.data.search.SavingsAccountTransactionSearchData;
+import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
+import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/savingsaccounts/{savingsId}/transactions")
+@Component
+@Tag(name = "Savings Account Transactions Search")
+@RequiredArgsConstructor
+public class SavingsAccountTransactionsSearchApiResource {
+
+    private final SavingsAccountTransactionsSearchServiceImpl service;
+    private final DefaultToApiJsonSerializer<SavingsAccountTransactionSearchData> toApiJsonSerializer;
+
+    @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,

Review Comment:
   @Mk9894 I see.. would you please change than only to be this v1?
   
   The idea is v2 is using the new way of generating the swagger definitions and using the Spring data page and some other enhancements, it this is to be support the legacy pagination, it should remain v1.
   
   is that fine?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [fineract] galovics merged pull request #3192: FINERACT-1912 Support Pagination, sorting and filtering for Savings account transactions

Posted by "galovics (via GitHub)" <gi...@apache.org>.
galovics merged PR #3192:
URL: https://github.com/apache/fineract/pull/3192


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@fineract.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org