diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java deleted file mode 100644 index 67977e4b2..000000000 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.core.data; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public enum RangeOperator { - - GTE(">="), LTE("<="), GT(">"), LT("<"); - - private final String symbol; -} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java index fd9f7b56f..12c3ff881 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Optional; import lombok.Data; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @Data @@ -44,7 +43,7 @@ public class PagedRequest { return Optional.ofNullable(request); } - public Pageable toPageable() { + public PageRequest toPageable() { if (isEmpty(sorts)) { return PageRequest.of(page, size); } else { @@ -57,16 +56,11 @@ public class PagedRequest { @SuppressWarnings({ "unused" }) private static class SortOrder { - private Direction direction; + private Sort.Direction direction; private String property; - private enum Direction { - ASC, DESC; - } - private Sort.Order toOrder() { - Sort.Direction d = Sort.Direction.fromString(direction.name()); - return new Sort.Order(d, property); + return new Sort.Order(direction, property); } } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java index cce2807b9..115af018c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java @@ -41,11 +41,14 @@ public class DatabaseSpecificSQLGenerator { this.databaseTypeResolver = databaseTypeResolver; } + public DatabaseType getDialect() { + return databaseTypeResolver.databaseType(); + } + public String escape(String arg) { - DatabaseType dialect = databaseTypeResolver.databaseType(); - if (dialect.isMySql()) { + if (databaseTypeResolver.isMySQL()) { return format("`%s`", arg); - } else if (dialect.isPostgres()) { + } else if (databaseTypeResolver.isPostgreSQL()) { return format("\"%s\"", arg); } return arg; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ColumnFilter.java similarity index 69% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ColumnFilter.java index 4522f0169..7b72f2085 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ColumnFilter.java @@ -16,14 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.portfolio.savings.domain.search; +package org.apache.fineract.infrastructure.dataqueries.data; -import java.util.Map; +import java.io.Serializable; import lombok.Data; +import lombok.NoArgsConstructor; +/** + * Immutable data object representing datatable data. + */ @Data -public class BaseQueryParametersMapResult { +@NoArgsConstructor +public final class ColumnFilter implements Serializable { + + private String columnName; + + private String columnValue; + + private String columnOperation; - private final String baseQueryString; - private final Map parametersMap; } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java index c9aa0e230..48f5e00e6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java @@ -99,6 +99,10 @@ public enum SavingsAccountTransactionType { return transactionType == null ? INVALID : transactionType; } + public boolean isValid() { + return this != INVALID; + } + public boolean isDeposit() { return this == DEPOSIT; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java index 3e5afd9de..37e604931 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java @@ -30,6 +30,8 @@ import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; +import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; +import org.apache.fineract.infrastructure.core.service.database.SqlOperator; import org.apache.fineract.infrastructure.dataqueries.data.DatatableCheckStatusData; import org.apache.fineract.infrastructure.dataqueries.data.DatatableChecksData; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; @@ -172,11 +174,12 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat return this.jdbcTemplate.query(sql, this.registerDataTableMapper); // NOSONAR } - protected static final class RegisterDataTableMapper implements RowMapper { + protected final class RegisterDataTableMapper implements RowMapper { + + public static final String SELECT_FROM = " t.application_table_name as entity, t.registered_table_name as tableName FROM x_registered_table t WHERE "; @Override public DatatableChecksData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final String entity = rs.getString("entity"); final String tableName = rs.getString("tableName"); @@ -184,8 +187,8 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat } public String schema() { - return " t.application_table_name as entity, t.registered_table_name as tableName " + " from x_registered_table t " - + " where application_table_name IN( 'm_client','m_group','m_savings_account','m_loan')"; + String[] values = EntityTables.getFiltered(EntityTables::hasCheck).stream().map(EntityTables::getName).toArray(String[]::new); + return SELECT_FROM + SqlOperator.IN.formatSql(sqlGenerator, JdbcJavaType.VARCHAR, "application_table_name", null, values); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index e95061c30..e10aa9a06 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.dataqueries.service; import com.google.gson.JsonObject; +import jakarta.validation.constraints.NotNull; import java.util.List; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -32,8 +33,6 @@ public interface ReadWriteNonCoreDataService { DatatableData retrieveDatatable(String datatable); - List queryDataTable(String datatable, String columnFilter, String valueFilter, String resultColumns); - @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')") void registerDatatable(JsonCommand command); @@ -73,4 +72,7 @@ public interface ReadWriteNonCoreDataService { String getDataTableName(String Url); Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn); + + List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue, + @NotNull String resultColumns); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 000433a52..bf2a82db6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -210,8 +210,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformApiDataValidationException(errors); } - DatabaseType dialect = databaseTypeResolver.databaseType(); - Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, dialect); + Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, false, sqlGenerator); String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " + EQ.formatPlaceholder(sqlGenerator, columnName, null); SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); @@ -1812,7 +1811,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ String queryParamColumnUnderscored; String columnHeaderUnderscored; boolean notFound; - DatabaseType dialect = databaseTypeResolver.databaseType(); final Map affectedColumns = new HashMap<>(); final Set keys = queryParams.keySet(); @@ -1833,7 +1831,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) { pValue = queryParams.get(key); validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat, - clientApplicationLocale, dialect); + clientApplicationLocale, true, sqlGenerator); affectedColumns.put(columnHeader.getColumnName(), validatedValue); notFound = false; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index bbbc66218..a1ed7911d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -37,7 +37,9 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; +import java.math.BigDecimal; import java.util.Collection; +import java.util.Locale; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.commands.domain.CommandWrapper; @@ -49,18 +51,19 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; -import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl; +import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService; +import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; import org.springframework.dao.CannotAcquireLockException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; @@ -76,7 +79,7 @@ public class SavingsAccountTransactionsApiResource { private final ApiRequestParameterHelper apiRequestParameterHelper; private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; private final PaymentTypeReadPlatformService paymentTypeReadPlatformService; - private final SavingsAccountTransactionsSearchServiceImpl transactionsSearchServiceImpl; + private final SavingsAccountTransactionSearchService transactionsSearchService; private boolean is(final String commandParam, final String commandValue) { return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); @@ -124,16 +127,35 @@ public class SavingsAccountTransactionsApiResource { SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS); } - @POST + @GET @Path("search") - @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Search Savings Account Transactions") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class))) }) - public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savingsId") final Long savingsId, - @Parameter PagedRequest searchRequest) { - Page transactionsData = transactionsSearchServiceImpl.searchTransactions(savingsId, searchRequest); + public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savings account id") final Long savingsId, + @QueryParam("fromDate") @Parameter(description = "minimum value date (inclusive)", example = "2023-08-08") final String fromDate, + @QueryParam("toDate") @Parameter(description = "maximum value date (inclusive)", example = "2023-08-15") final String toDate, + @QueryParam("fromSubmittedDate") @Parameter(description = "minimum booking date (inclusive)", example = "2023-08-08") final String fromSubmittedDate, + @QueryParam("toSubmittedDate") @Parameter(description = "maximum booking date (inclusive)", example = "2023-08-15") final String toSubmittedDate, + @QueryParam("fromAmount") @Parameter(description = "minimum transaction amount (inclusive)", example = "1000") final BigDecimal fromAmount, + @QueryParam("toAmount") @Parameter(description = "maximum transaction amount (inclusive)", example = "50000000") final BigDecimal toAmount, + @QueryParam("types") @Parameter(description = "transaction types", example = "1,2,4,20,21") final String types, + @QueryParam("credit") @Parameter(description = "credit") final Boolean credit, + @QueryParam("debit") @Parameter(description = "debit") final Boolean debit, + @QueryParam("offset") @Parameter(description = "offset") final Integer offset, + @QueryParam("limit") @Parameter(description = "limit") final Integer limit, + @QueryParam("orderBy") @Parameter(description = "sort properties", example = "createdDate,transactionDate,id") final String orderBy, + @QueryParam("sortOrder") @Parameter(description = "sort direction") final Sort.Direction sortOrder, + @QueryParam("locale") @Parameter(description = "locale") final String localeString, + @QueryParam("dateFormat") @Parameter(description = "date format", example = "yyyy-MM-dd") String dateFormat) { + final Locale locale = localeString == null ? null : JsonParserHelper.localeFromString(localeString); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().accountId(savingsId) + .fromDate(fromDate, dateFormat, locale).toDate(toDate, dateFormat, locale) + .fromSubmittedDate(fromSubmittedDate, dateFormat, locale).toSubmittedDate(toSubmittedDate, dateFormat, locale) + .fromAmount(fromAmount).toAmount(toAmount).types(types).credit(credit).debit(debit) + .pageable(offset, limit, orderBy, sortOrder); + Page transactionsData = transactionsSearchService.searchTransactions(savingsId, searchParameters); return toApiJsonSerializer.serialize(transactionsData); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java index 80bd34c9d..3e7f53239 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java @@ -185,8 +185,8 @@ final class SavingsAccountTransactionsApiResourceSwagger { } @Schema(example = "2") - public Long totalFilteredRecords; - public Set pageItems; + public Long total; + public Set content; } @Schema(description = "PostSavingsAccountTransactionsRequest") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java deleted file mode 100644 index 39a17334a..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.data; - -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.data.RangeOperator; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter; -import org.springframework.stereotype.Component; - -@Component -public class SavingsAccountTransactionSearchValidator { - - public void validateSearchFilters(SavingsTransactionSearch.Filters searchFilters) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME); - if (searchFilters != null) { - - List> dateFilters = searchFilters.getTransactionDate(); - validateRangeFilters(baseDataValidator, dateFilters, transactionDateParamName); - - List> amountFilters = searchFilters.getTransactionAmount(); - validateRangeFilters(baseDataValidator, amountFilters, transactionAmountParamName); - } - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void validateRangeFilters(DataValidatorBuilder baseDataValidator, List> rangeFilters, String paramName) { - if (rangeFilters == null) { - return; - } - - if (rangeFilters.size() > 2) { - baseDataValidator.parameter(paramName).value(rangeFilters).notExceedingListLengthOf(2); - } - - if (!rangeFilters.isEmpty()) { - RangeFilter firstFilter = rangeFilters.get(0); - RangeOperator firstOperator = firstFilter.getOperator(); - - if (rangeFilters.size() == 2) { - RangeFilter secondFilter = rangeFilters.get(1); - RangeOperator secondOperator = secondFilter.getOperator(); - - if (((firstOperator == RangeOperator.GT || firstOperator == RangeOperator.GTE) - && !(secondOperator == RangeOperator.LT || secondOperator == RangeOperator.LTE)) - || ((firstOperator == RangeOperator.LT || firstOperator == RangeOperator.LTE) - && !(secondOperator == RangeOperator.GT || secondOperator == RangeOperator.GTE))) { - baseDataValidator.parameter(paramName).failWithCode("invalid.range", firstOperator, secondOperator); - } - } - } - - for (RangeFilter filter : rangeFilters) { - T value = filter.getValue(); - if (value instanceof BigDecimal) { - baseDataValidator.parameter(paramName).value(value).zeroOrPositiveAmount(); - } - } - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java deleted file mode 100644 index 4ab2ad47c..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.data; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.Objects; -import java.util.Optional; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; -import org.apache.fineract.portfolio.account.data.AccountTransferData; -import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction; -import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; -import org.apache.fineract.useradministration.domain.AppUser; - -@Getter -@AllArgsConstructor -public class SavingsTransactionSearchResult { - - private Long transactionId; - private Integer transactionType; - private LocalDate transactionDate; - private BigDecimal transactionAmount; - private Long releaseIdOfHoldAmountTransaction; - private String reasonForBlock; - private LocalDateTime createdDate; - private AppUser appUser; - private String note; - private BigDecimal runningBalance; - private boolean reversed; - private boolean reversalTransaction; - private Long originalTxnId; - private Boolean lienTransaction; - private boolean isManualTransaction; - private AccountTransferTransaction fromSavingsTransaction; - private AccountTransferTransaction toSavingsTransaction; - private SavingsAccount savingsAccount; - private PaymentDetail paymentDetail; - private ApplicationCurrency currency; - - public static final SavingsAccountTransactionData toSavingsAccountTransactionData(SavingsTransactionSearchResult dto) { - final Long id = dto.getTransactionId(); - final int transactionTypeInt = dto.getTransactionType(); - - final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); - - final LocalDate date = dto.getTransactionDate(); - final LocalDate submittedOnDate = Optional.ofNullable(dto.getCreatedDate()).map(LocalDateTime::toLocalDate).orElse(null); - final BigDecimal amount = Optional.ofNullable(dto.getTransactionAmount()).orElse(BigDecimal.ZERO); - final Long releaseTransactionId = Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(null); - final String reasonForBlock = Optional.ofNullable(dto.getReasonForBlock()).orElse(null); - final BigDecimal outstandingChargeAmount = null; - final BigDecimal runningBalance = Optional.ofNullable(dto.getRunningBalance()).orElse(BigDecimal.ZERO); - final boolean reversed = dto.isReversed(); - final boolean isReversal = dto.isReversalTransaction(); - final Long originalTransactionId = Optional.ofNullable(dto.getOriginalTxnId()).orElse(null); - final Boolean lienTransaction = dto.getLienTransaction(); - - final Long savingsId = Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount -> savingsAccount.getId()).orElse(null); - final String accountNo = Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount -> savingsAccount.getAccountNumber()) - .orElse(null); - final boolean postInterestAsOn = dto.isManualTransaction(); - - PaymentDetailData paymentDetailData = null; - if (Objects.nonNull(transactionType) && transactionType.isDepositOrWithdrawal()) { - final PaymentDetail paymentDetail = dto.getPaymentDetail(); - if (Objects.nonNull(paymentDetail)) { - final Long paymentTypeId = Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType -> paymentType.getId()) - .orElse(null); - if (Objects.nonNull(paymentTypeId)) { - final String typeName = Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType -> paymentType.getName()) - .orElse(null); - final PaymentTypeData paymentType = PaymentTypeData.instance(paymentTypeId, typeName); - final String accountNumber = paymentDetail.getAccountNumber(); - final String checkNumber = paymentDetail.getCheckNumber(); - final String routingCode = paymentDetail.getRoutingCode(); - final String receiptNumber = paymentDetail.getReceiptNumber(); - final String bankNumber = paymentDetail.getBankNumber(); - paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, - bankNumber); - } - } - } - - final String currencyCode = dto.getSavingsAccount().getCurrency().getCode(); - final String currencyName = dto.getCurrency().getName(); - final String currencyNameCode = dto.getCurrency().getNameCode(); - final String currencyDisplaySymbol = dto.getCurrency().getDisplaySymbol(); - final Integer currencyDigits = dto.getSavingsAccount().getCurrency().getDigitsAfterDecimal(); - final Integer inMultiplesOf = dto.getSavingsAccount().getCurrency().getCurrencyInMultiplesOf(); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - - AccountTransferData transfer = null; - AccountTransferTransaction transferFrom = dto.getFromSavingsTransaction(); - AccountTransferTransaction transferTo = dto.getToSavingsTransaction(); - if (Objects.nonNull(transferFrom)) { - final Long fromTransferId = transferFrom.getId(); - final LocalDate fromTransferDate = transferFrom.getDate(); - final BigDecimal fromTransferAmount = Optional.ofNullable(transferFrom.getAmount()).orElse(BigDecimal.ZERO); - final boolean fromTransferReversed = transferFrom.isReversed(); - final String fromTransferDescription = transferFrom.getDescription(); - - transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, - fromTransferDescription, fromTransferReversed); - } else if (Objects.nonNull(transferTo)) { - final Long toTransferId = transferTo.getId(); - final LocalDate toTransferDate = transferTo.getDate(); - final BigDecimal toTransferAmount = Optional.ofNullable(transferTo.getAmount()).orElse(BigDecimal.ZERO); - final boolean toTransferReversed = transferTo.isReversed(); - final String toTransferDescription = transferTo.getDescription(); - - transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, - toTransferDescription, toTransferReversed); - } - final String submittedByUsername = Optional.ofNullable(dto.getAppUser()).map(user -> user.getUsername()).orElse(null); - final String note = Optional.ofNullable(dto.getNote()).orElse(null); - return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, amount, - outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, submittedByUsername, note, - isReversal, originalTransactionId, lienTransaction, releaseTransactionId, reasonForBlock); - - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java index ae0709658..2eba64bb2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java @@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.savings.domain; import jakarta.persistence.LockModeType; import java.time.LocalDate; import java.util.List; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionsSearchRepository; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -29,8 +28,8 @@ import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface SavingsAccountTransactionRepository extends JpaRepository, - JpaSpecificationExecutor, SavingsTransactionsSearchRepository { +public interface SavingsAccountTransactionRepository + extends JpaRepository, JpaSpecificationExecutor { @Query("select sat from SavingsAccountTransaction sat where sat.id = :transactionId and sat.savingsAccount.id = :savingsId") SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java deleted file mode 100644 index ef09050bb..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.domain.search; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.List; -import lombok.Data; -import org.apache.fineract.infrastructure.core.data.RangeOperator; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; - -@Data -public class SavingsTransactionSearch { - - private Filters filters; - - @Data - public static class Filters { - - private List> transactionDate; - - private List> transactionAmount; - - private List transactionType; - } - - @Data - public static class RangeFilter { - - private RangeOperator operator; - - private T value; - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java deleted file mode 100644 index 4ec2f4dd8..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.domain.search; - -import lombok.Builder; -import lombok.Getter; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; -import org.springframework.data.domain.Pageable; - -@Getter -@Builder -public class SavingsTransactionSearchParameters { - - private Long savingsId; - - private DepositAccountType depositAccountType; - - private Filters filters; - - private Pageable pageable; - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java deleted file mode 100644 index 8a881154e..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.domain.search; - -import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; -import org.springframework.data.domain.Page; - -public interface SavingsTransactionsSearchRepository { - - Page searchTransactions(SavingsTransactionSearchParameters searchParameters); -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java deleted file mode 100644 index 2a92fea05..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.domain.search; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class SavingsTransactionsSearchRepositoryImpl implements SavingsTransactionsSearchRepository { - - private static final String AMOUNT_FIELD_NAME = "amount"; - private static final String ID_FIELD_NAME = "id"; - private static final String CREATED_DATE_FIELD_NAME = "createdDate"; - private static final String TRANSACTION_DATE_FIELD_NAME = "dateOf"; - private final EntityManager entityManager; - - @Override - public Page searchTransactions(SavingsTransactionSearchParameters searchParameters) { - // Build base query with filters but without the selection - BaseQueryParametersMapResult baseQueryParameterMapResult = buildBaseQueryWithFilters(searchParameters.getSavingsId(), - searchParameters.getDepositAccountType(), searchParameters.getFilters()); - - // Attach the selection - String jpqlQuery = attachSelection(baseQueryParameterMapResult.getBaseQueryString()); - - // Attach the ordering - String queryWithOrdering = attachOrdering(jpqlQuery, searchParameters.getPageable().getSort()); - - // Execute Query - TypedQuery queryToExecute = entityManager.createQuery(queryWithOrdering, - SavingsTransactionSearchResult.class); - setQueryParameters(queryToExecute, baseQueryParameterMapResult.getParametersMap()); - applyPagination(queryToExecute, searchParameters.getPageable()); - List resultList = queryToExecute.getResultList(); - - // Attach the count selection - String countQuery = attachCountSelection(baseQueryParameterMapResult.getBaseQueryString()); - - // Execute count query - TypedQuery countQueryToExecute = entityManager.createQuery(countQuery, Long.class); - setQueryParameters(countQueryToExecute, baseQueryParameterMapResult.getParametersMap()); - Long totalElements = countQueryToExecute.getSingleResult(); - - return PageableExecutionUtils.getPage(resultList, searchParameters.getPageable(), () -> totalElements); - } - - private void setQueryParameters(TypedQuery queryToExecute, Map parametersMap) { - for (Map.Entry entry : parametersMap.entrySet()) { - queryToExecute.setParameter(entry.getKey(), entry.getValue()); - } - } - - private BaseQueryParametersMapResult buildBaseQueryWithFilters(Long savingsId, DepositAccountType depositAccountType, Filters filters) { - String baseQuery = """ - SELECT tr - FROM SavingsAccountTransaction tr - JOIN ApplicationCurrency currency ON (currency.code = tr.savingsAccount.currency.code) - LEFT JOIN AccountTransferTransaction fromtran ON (fromtran.fromSavingsTransaction = tr) - LEFT JOIN AccountTransferTransaction totran ON (totran.toSavingsTransaction = tr) - LEFT JOIN tr.notes nt ON (nt.savingsTransaction = tr) - WHERE tr.savingsAccount.id = :savingsId - AND tr.savingsAccount.depositType = :depositType - """; - StringBuilder baseQueryBuilder = new StringBuilder(baseQuery); - - Map parameterMap = new HashMap<>(); - parameterMap.put("savingsId", savingsId); - parameterMap.put("depositType", depositAccountType.getValue()); - - setFilterConditions(baseQueryBuilder, parameterMap, filters); - return new BaseQueryParametersMapResult(baseQueryBuilder.toString(), parameterMap); - } - - private String attachSelection(String baseQuery) { - return baseQuery.replace("SELECT tr", - "SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult(tr.id,tr.typeOf, tr.dateOf, tr.amount, tr.releaseIdOfHoldAmountTransaction, tr.reasonForBlock,tr.createdDate, tr.appUser, nt.note, tr.runningBalance, tr.reversed,tr.reversalTransaction, tr.originalTxnId, tr.lienTransaction, tr.isManualTransaction,fromTran, toTran, tr.savingsAccount, tr.paymentDetail, currency) "); - } - - private String attachOrdering(String jpqlQuery, Sort sort) { - StringJoiner orderByClauseBuilder = new StringJoiner(", ", " ORDER BY ", ""); - - if (Objects.nonNull(sort) && sort.isSorted()) { - buildOrderByClause(sort.toList(), orderByClauseBuilder); - } else { - List defaultOrders = getDefaultOrders(); - buildOrderByClause(defaultOrders, orderByClauseBuilder); - } - return new StringBuilder(jpqlQuery).append(orderByClauseBuilder.toString()).toString(); - } - - private void buildOrderByClause(List orders, StringJoiner orderByClauseBuilder) { - for (Order order : orders) { - String property = "tr." + order.getProperty(); - String direction = order.getDirection().name(); - String orderByExpression = new StringBuilder(property).append(StringUtils.SPACE).append(direction).toString(); - orderByClauseBuilder.add(orderByExpression); - } - } - - private List getDefaultOrders() { - return List.of(Order.desc(TRANSACTION_DATE_FIELD_NAME), Order.desc(CREATED_DATE_FIELD_NAME), Order.desc(ID_FIELD_NAME)); - } - - private void applyPagination(TypedQuery query, Pageable pageable) { - if (pageable.isPaged()) { - query.setFirstResult((int) pageable.getOffset()); - query.setMaxResults(pageable.getPageSize()); - } - } - - private String attachCountSelection(String baseQuery) { - return baseQuery.replace("SELECT tr", "SELECT COUNT(tr) "); - } - - private void setFilterConditions(StringBuilder queryBuilder, Map parameterMap, Filters filters) { - if (Objects.nonNull(filters)) { - List> dateFilters = filters.getTransactionDate(); - List> amountFilters = filters.getTransactionAmount(); - List transactionTypes = filters.getTransactionType(); - - if (Objects.nonNull(dateFilters)) { - processRangeFilters(queryBuilder, parameterMap, dateFilters, TRANSACTION_DATE_FIELD_NAME); - } - - if (Objects.nonNull(amountFilters)) { - processRangeFilters(queryBuilder, parameterMap, amountFilters, AMOUNT_FIELD_NAME); - } - - if (CollectionUtils.isNotEmpty(transactionTypes)) { - List transactionTypeValues = transactionTypes.stream().map(SavingsAccountTransactionType::getValue) - .collect(Collectors.toList()); - queryBuilder.append(" AND tr.typeOf IN :transactionTypes "); - parameterMap.put("transactionTypes", transactionTypeValues); - } - } - } - - private void processRangeFilters(StringBuilder queryBuilder, Map parameterMap, List> filters, - String field) { - filters.forEach(filter -> { - String paramName = new StringBuilder(field).append(filter.getOperator()).toString(); - queryBuilder.append(" AND tr.").append(field).append(StringUtils.SPACE).append(filter.getOperator().getSymbol()).append(" :") - .append(paramName); - parameterMap.put(paramName, filter.getValue()); - }); - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 9740bbec7..e3835b879 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -1313,52 +1313,54 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead * return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); } */ - private static final class SavingsAccountTransactionsMapper implements RowMapper { + public static final class SavingsAccountTransactionsMapper implements RowMapper { - private final String schemaSql; + private static final String SELECT = buildSelect(); + private static final String FROM = buildFrom(); + private static final String SCHEMA = SELECT + FROM; - SavingsAccountTransactionsMapper() { + public SavingsAccountTransactionsMapper() {} - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); - sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); - sqlBuilder.append(" tr.release_id_of_hold_amount as releaseTransactionId,"); - sqlBuilder.append(" tr.reason_for_block as reasonForBlock,"); - sqlBuilder.append("tr.submitted_on_date as submittedOnDate,"); - sqlBuilder.append(" au.username as submittedByUsername, "); - sqlBuilder.append(" nt.note as transactionNote, "); - sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); - sqlBuilder.append( - "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, "); - sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,"); - sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"); - sqlBuilder.append("fromtran.description as fromTransferDescription,"); - sqlBuilder.append("totran.id as toTransferId, totran.is_reversed as toTransferReversed,"); - sqlBuilder.append("totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,"); - sqlBuilder.append("totran.description as toTransferDescription,"); - sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,"); - sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); - sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); - sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); - sqlBuilder.append("pt.value as paymentTypeName, "); - sqlBuilder.append("tr.is_manual as postInterestAsOn "); - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id "); - sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - sqlBuilder.append("left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id "); - sqlBuilder.append("left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id "); - sqlBuilder.append("left join m_payment_detail pd on tr.payment_detail_id = pd.id "); - sqlBuilder.append("left join m_payment_type pt on pd.payment_type_id = pt.id "); - sqlBuilder.append(" left join m_appuser au on au.id=tr.appuser_id "); - sqlBuilder.append(" left join m_note nt ON nt.savings_account_transaction_id=tr.id "); - this.schemaSql = sqlBuilder.toString(); + private static String buildSelect() { + return "tr.id as transactionId, tr.transaction_type_enum as transactionType, " + + "tr.transaction_date as transactionDate, tr.amount as transactionAmount, " + + "tr.release_id_of_hold_amount as releaseTransactionId, tr.reason_for_block as reasonForBlock, " + + "tr.submitted_on_date as submittedOnDate, au.username as submittedByUsername, nt.note as transactionNote, " + + "tr.running_balance_derived as runningBalance, tr.is_reversed as reversed, " + + "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, " + + "fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed, " + + "fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount, " + + "fromtran.description as fromTransferDescription, " + + "totran.id as toTransferId, totran.is_reversed as toTransferReversed, " + + "totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, " + + "totran.description as toTransferDescription, sa.id as savingsId, sa.account_no as accountNo, " + + "pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, " + + "pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, " + + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, " + + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, " + + "curr.display_symbol as currencyDisplaySymbol, pt.value as paymentTypeName, " + "tr.is_manual as postInterestAsOn "; + } + + private static String buildFrom() { + return " FROM m_savings_account_transaction tr join m_savings_account sa on tr.savings_account_id = sa.id " + + "join m_currency curr on curr.code = sa.currency_code " + + "left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id " + + "left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id " + + "left join m_payment_detail pd on tr.payment_detail_id = pd.id " + + "left join m_payment_type pt on pd.payment_type_id = pt.id left join m_appuser au on au.id=tr.appuser_id " + + "left join m_note nt ON nt.savings_account_transaction_id=tr.id "; } public String schema() { - return this.schemaSql; + return SCHEMA; + } + + public String select() { + return SELECT; + } + + public String from() { + return FROM; } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java index e330aeaed..3bf5765cf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java @@ -18,13 +18,12 @@ */ package org.apache.fineract.portfolio.savings.service.search; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.core.service.PagedRequest; +import jakarta.validation.constraints.NotNull; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; +import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; +import org.springframework.data.domain.Page; public interface SavingsAccountTransactionSearchService { - Page searchTransactions(Long savingsId, PagedRequest searchRequest); - + Page searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java index 0d477e00c..ba06548d8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java @@ -18,21 +18,37 @@ */ package org.apache.fineract.portfolio.savings.service.search; -import java.util.Objects; -import java.util.Optional; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; + +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.core.service.database.SqlOperator; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; +import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionSearchValidator; -import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; -import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearchParameters; -import org.springframework.data.domain.Pageable; +import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformServiceImpl; +import org.apache.fineract.portfolio.search.data.ColumnFilterData; +import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; +import org.apache.fineract.portfolio.search.service.SearchUtil; +import org.jetbrains.annotations.Nullable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,35 +58,117 @@ import org.springframework.transaction.annotation.Transactional; public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccountTransactionSearchService { private final PlatformSecurityContext context; - - private final SavingsAccountTransactionRepository savingsTransactionRepository; - - private final SavingsAccountTransactionSearchValidator searchValidator; + private final GenericDataService genericDataService; + private final DatabaseSpecificSQLGenerator sqlGenerator; + private final JdbcTemplate jdbcTemplate; @Override - public Page searchTransactions(Long savingsId, PagedRequest searchRequest) { - validateSearchRequest(searchRequest); - return executeSearch(savingsId, DepositAccountType.SAVINGS_DEPOSIT, searchRequest); + public Page searchTransactions(@NotNull Long savingsId, + @NotNull TransactionSearchRequest searchParameters) { + context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME); + + String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName(); + Map columnHeaders = SearchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(apptable)); + + PageRequest pageable = searchParameters.getPageable(); + PageRequest sortPageable; + if (pageable.getSort().isSorted()) { + List errors = new ArrayList<>(); + List orders = pageable.getSort().toList(); + sortPageable = pageable.withSort(Sort.by(orders.stream() + .map(e -> e.withProperty(SearchUtil.validateToJdbcColumn(e.getProperty(), columnHeaders, errors, false))).toList())); + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException(errors); + } + } else { + pageable = pageable.withSort(Sort.Direction.DESC, "transaction_date", "created_date", "id"); + sortPageable = pageable; + } + + List columnFilters = new ArrayList<>(); + columnFilters.add(ColumnFilterData.eq("savings_account_id", savingsId.toString())); + columnFilters.add(ColumnFilterData.eq("is_reversal", Boolean.FALSE.toString())); + addFromToFilter("transaction_date", DateUtils.format(searchParameters.getFromDate()), + DateUtils.format(searchParameters.getToDate()), columnFilters); + addFromToFilter("submitted_on_date", DateUtils.format(searchParameters.getFromSubmittedDate()), + DateUtils.format(searchParameters.getToSubmittedDate()), columnFilters); + addFromToFilter("amount", MathUtil.formatToSql(searchParameters.getFromAmount()), + MathUtil.formatToSql(searchParameters.getToAmount()), columnFilters); + + Page emptyResult = PageableExecutionUtils.getPage(new ArrayList<>(0), pageable, () -> 0); + if (addTransactionTypesFilter(searchParameters, columnFilters) == null) { + return emptyResult; + } + + String alias = "tr"; + StringBuilder where = new StringBuilder(" WHERE "); + ArrayList params = new ArrayList<>(); + SearchUtil.buildQueryCondition(columnFilters, where, params, alias, columnHeaders, false, false, sqlGenerator); + + SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper tm = new SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper(); + Object[] args = params.toArray(); + + String countQuery = "SELECT COUNT(*) " + tm.from() + where; + Integer totalElements = jdbcTemplate.queryForObject(countQuery, Integer.class, args); + if (totalElements == null || totalElements == 0) { + return emptyResult; + } + + StringBuilder query = new StringBuilder().append("SELECT ").append(tm.schema()).append(where); + query.append(" ").append(sqlGenerator.buildOrderBy(sortPageable.getSort().toList(), alias, false)); + if (pageable.isPaged()) { + query.append(" ").append(sqlGenerator.limit(pageable.getPageSize(), (int) pageable.getOffset())); + } + + List results = this.jdbcTemplate.query(query.toString(), tm, args); + return PageableExecutionUtils.getPage(results, pageable, () -> totalElements); } - private void validateSearchRequest(PagedRequest searchRequest) { - Objects.requireNonNull(searchRequest, "searchRequest must not be null"); - context.isAuthenticated(); - Optional request = searchRequest.getRequest(); - Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); - searchValidator.validateSearchFilters(searchFilters); + private static void addFromToFilter(@NotNull String column, String fromValue, String toValue, + @NotNull List columnFilters) { + if (fromValue != null) { + columnFilters.add(toValue == null ? ColumnFilterData.create(column, SqlOperator.GTE, fromValue) + : ColumnFilterData.btw(column, fromValue, toValue)); + } else if (toValue != null) { + columnFilters.add(ColumnFilterData.create(column, SqlOperator.LTE, toValue)); + } } - private Page executeSearch(Long savingsId, DepositAccountType depositType, - PagedRequest searchRequest) { - Optional request = searchRequest.getRequest(); - Pageable pageable = searchRequest.toPageable(); - Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); - SavingsTransactionSearchParameters searchParameters = SavingsTransactionSearchParameters.builder().savingsId(savingsId) - .depositAccountType(DepositAccountType.SAVINGS_DEPOSIT).filters(searchFilters).pageable(pageable).build(); - org.springframework.data.domain.Page pageResult = savingsTransactionRepository - .searchTransactions(searchParameters).map(SavingsTransactionSearchResult::toSavingsAccountTransactionData); - return new Page<>(pageResult.getContent(), Long.valueOf(pageResult.getTotalElements()).intValue()); - } + @Nullable + private static Boolean addTransactionTypesFilter(@NotNull TransactionSearchRequest searchParameters, + List columnFilters) { + Predicate filter = null; + Boolean credit = searchParameters.getCredit(); + Boolean debit = searchParameters.getDebit(); + if (credit != null) { + Predicate cf = SavingsAccountTransactionType::isCreditEntryType; + filter = credit ? cf : Predicate.not(cf); + } + if (debit != null) { + Predicate df = SavingsAccountTransactionType::isDebitEntryType; + if (!debit) { + df = Predicate.not(df); + } + filter = credit == null ? df : (credit && debit ? filter.or(df) : filter.and(df)); + } + if (searchParameters.getTypes() != null) { + List types = Arrays.asList(searchParameters.getTypes()); + Predicate tf = t -> types.contains(String.valueOf(t.getId())); + filter = filter == null ? tf : filter.and(tf); + } + if (filter != null) { + filter = filter.and(SavingsAccountTransactionType::isValid); + List filteredTypes = SavingsAccountTransactionType.getFiltered(filter); + if (filteredTypes.isEmpty()) { + return null; + } else { + String[] values = filteredTypes.stream().map(t -> String.valueOf(t.getId())).toArray(String[]::new); + columnFilters.add(ColumnFilterData.create("transaction_type_enum", SqlOperator.IN, values)); + return true; + } + } + return false; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java index 48a327c58..dbe099447 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java @@ -23,7 +23,14 @@ import java.util.Set; public final class SearchConstants { + public static final String API_PARAM_RESULTCOLUMNS = "resultColumns"; + public static final String API_PARAM_COLUMN_FILTERS = "columnFilters"; public static final String API_PARAM_COLUMN = "column"; + public static final String API_PARAM_FILTERS = "filters"; + public static final String API_PARAM_OPERATOR = "operator"; + public static final String API_PARAM_VALUES = "values"; + public static final String API_PARAM_TABLE = "table"; + public static final String API_PARAM_QUERY = "query"; private SearchConstants() {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/ColumnFilterData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/ColumnFilterData.java new file mode 100644 index 000000000..a4c7ba746 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/ColumnFilterData.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.search.data; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.fineract.infrastructure.core.service.database.SqlOperator; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public final class ColumnFilterData implements Serializable { + + private String column; + + private List filters; + + public static ColumnFilterData eq(String column, String value) { + return new ColumnFilterData(column, List.of(FilterData.eq(value))); + } + + public static ColumnFilterData btw(String column, String value1, String value2) { + return new ColumnFilterData(column, List.of(FilterData.btw(value1, value2))); + } + + public static ColumnFilterData create(String column, SqlOperator op, String... values) { + return new ColumnFilterData(column, List.of(FilterData.create(op, values))); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/FilterData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/FilterData.java new file mode 100644 index 000000000..58345f2bc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/FilterData.java @@ -0,0 +1,54 @@ +/** + * 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.search.data; + +import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.BTW; +import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.EQ; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.fineract.infrastructure.core.service.database.SqlOperator; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public final class FilterData implements Serializable { + + private SqlOperator operator; + + private List values; + + static FilterData eq(String value) { + return new FilterData(EQ, List.of(value)); + } + + static FilterData btw(String value1, String value2) { + return new FilterData(BTW, List.of(value1, value2)); + } + + static FilterData create(SqlOperator op, String... values) { + return new FilterData(op, List.of(values)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TransactionSearchRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TransactionSearchRequest.java new file mode 100644 index 000000000..5b4953507 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TransactionSearchRequest.java @@ -0,0 +1,106 @@ +/** + * 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.search.data; + +import static org.apache.fineract.portfolio.search.service.SearchUtil.DEFAULT_PAGE_SIZE; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Locale; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.logging.log4j.util.Strings; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +@Getter +public class TransactionSearchRequest { + + private Long accountId; + private LocalDate fromDate; + private LocalDate toDate; + private LocalDate fromSubmittedDate; + private LocalDate toSubmittedDate; + private BigDecimal fromAmount; + private BigDecimal toAmount; + private String[] types; + private Boolean credit; + private Boolean debit; + + private PageRequest pageable; + + public TransactionSearchRequest accountId(Long accountId) { + this.accountId = accountId; + return this; + } + + public TransactionSearchRequest fromDate(String fromDate, String dateFormat, Locale locale) { + this.fromDate = fromDate == null ? null : DateUtils.parseLocalDate(fromDate, dateFormat, locale); + return this; + } + + public TransactionSearchRequest toDate(String toDate, String dateFormat, Locale locale) { + this.toDate = toDate == null ? null : DateUtils.parseLocalDate(toDate, dateFormat, locale); + return this; + } + + public TransactionSearchRequest fromSubmittedDate(String fromSubmittedDate, String dateFormat, Locale locale) { + this.fromSubmittedDate = fromSubmittedDate == null ? null : DateUtils.parseLocalDate(fromSubmittedDate, dateFormat, locale); + return this; + } + + public TransactionSearchRequest toSubmittedDate(String toSubmittedDate, String dateFormat, Locale locale) { + this.toSubmittedDate = toSubmittedDate == null ? null : DateUtils.parseLocalDate(toSubmittedDate, dateFormat, locale); + return this; + } + + public TransactionSearchRequest fromAmount(BigDecimal fromAmount) { + this.fromAmount = fromAmount; + return this; + } + + public TransactionSearchRequest toAmount(BigDecimal toAmount) { + this.toAmount = toAmount; + return this; + } + + public TransactionSearchRequest types(String types) { + this.types = types == null ? null : types.split(","); + return this; + } + + public TransactionSearchRequest credit(Boolean credit) { + this.credit = credit; + return this; + } + + public TransactionSearchRequest debit(Boolean debit) { + this.debit = debit; + return this; + } + + public TransactionSearchRequest pageable(Integer offset, Integer limit, String orderByProps, Sort.Direction direction) { + offset = MathUtil.nullToDefault(offset, 0); + limit = MathUtil.nullToDefault(limit, DEFAULT_PAGE_SIZE); + String[] properties = Strings.isEmpty(orderByProps) ? null : orderByProps.split(","); + this.pageable = properties == null ? PageRequest.of(offset, limit) : PageRequest.of(offset, limit, direction, properties); + return this; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java index 87a5653fb..2c3ac2211 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java @@ -46,10 +46,13 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.database.DatabaseType; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; +import org.apache.fineract.infrastructure.core.service.database.SqlOperator; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator; +import org.apache.fineract.portfolio.search.data.ColumnFilterData; +import org.apache.fineract.portfolio.search.data.FilterData; import org.springframework.jdbc.support.rowset.SqlRowSet; public final class SearchUtil { @@ -112,6 +115,7 @@ public final class SearchUtil { } } + @NotNull public static List validateToJdbcColumns(List columns, Map columnHeaders, List errors, boolean allowEmpty) { List result = new ArrayList<>(); @@ -131,7 +135,6 @@ public final class SearchUtil { errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null)); } if (column != null) { - SQLInjectionValidator.validateDynamicQuery(column); if (!columnHeaders.containsKey(column)) { column = camelToSnake(column); if (!columnHeaders.containsKey(column)) { @@ -143,8 +146,64 @@ public final class SearchUtil { return column; } + public static boolean buildQueryCondition(List columnFilters, @NotNull StringBuilder where, + @NotNull List params, String alias, Map columnHeaders, boolean embedded, + boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + if (columnFilters == null) { + return false; + } + boolean added = false; + int isize = columnFilters.size(); + for (int i = 0; i < isize; i++) { + boolean addedFilter = buildFilterCondition(columnFilters.get(i), where, params, alias, columnHeaders, embedded, strict, + sqlGenerator); + if (addedFilter && i < isize - 1) { + where.append(" AND "); + } + added |= addedFilter; + } + return added; + } + + public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List params, + String alias, Map columnHeaders, boolean embedded, boolean strict, + @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + String columnName = columnFilter.getColumn(); + List filters = columnFilter.getFilters(); + int size = filters.size(); + for (int i = 0; i < size; i++) { + if (!embedded && where.isEmpty()) { + where.append(" WHERE "); + } + FilterData filter = filters.get(i); + SqlOperator operator = filter.getOperator(); + List values = filter.getValues(); + List objectValues = values == null ? null + : values.stream().map(e -> parseAndValidateJdbcColumnValue(columnName, e, columnHeaders, strict, sqlGenerator)) + .toList(); + + operator.validateValues(values); + if (operator.isPlaceholderSupported()) { + where.append(operator.formatPlaceholder(sqlGenerator, columnName, alias)); + if (objectValues != null) { + if (operator.isListType()) { + params.add(objectValues); + } else { + params.addAll(objectValues); + } + } + } else { + where.append(operator.formatSql(sqlGenerator, columnHeaders.get(columnName).getColumnType(), columnName, alias, values)); + } + if (i < size - 1) { + where.append(" AND "); + } + } + return size > 0; + } + public static Object parseAndValidateJdbcColumnValue(String column, String columnValue, - Map columnHeaders, DatabaseType dialect) { + Map columnHeaders, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { List errors = new ArrayList<>(); column = validateToJdbcColumn(column, columnHeaders, errors, false); if (!errors.isEmpty()) { @@ -152,12 +211,12 @@ public final class SearchUtil { } ResultsetColumnHeaderData columnHeader = columnHeaders.get(column); String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT; - return columnHeader.getColumnType().toJdbcValue(dialect, - parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, dialect), false); + return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(), + parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, strict, sqlGenerator), false); } public static Object parseAndValidateColumnValue(final ResultsetColumnHeaderData columnHeader, final String pValue, - final String dateFormat, final Locale locale, DatabaseType dialect) { + final String dateFormat, final Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { String columnValue = pValue; JdbcJavaType colType = columnHeader.getColumnType(); if (!colType.isStringType() || !columnHeader.isMandatory()) { @@ -173,7 +232,9 @@ public final class SearchUtil { if (StringUtils.isEmpty(columnValue)) { return columnValue; } - SQLInjectionValidator.validateDynamicQuery(columnValue); + if (strict) { + SQLInjectionValidator.validateDynamicQuery(columnValue); + } if (columnHeader.hasColumnValues()) { if (columnHeader.isCodeValueDisplayType()) { @@ -193,7 +254,7 @@ public final class SearchUtil { return codeLookup; } else { throw new PlatformDataIntegrityException("error.msg.invalid.columnType.", "Code: " + columnHeader.getColumnName() - + " - Invalid Type " + colType.getJdbcName(dialect) + " (neither varchar nor int)"); + + " - Invalid Type " + colType.getJdbcName(sqlGenerator.getDialect()) + " (neither varchar nor int)"); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java index caa5a9f9f..f38920ce8 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java @@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseType; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; -import org.apache.fineract.infrastructure.security.utils.SQLInjectionException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -75,7 +74,7 @@ public class ReadWriteNonCoreDataServiceImplTest { @Test public void testSqlInjectionCaughtQueryDataTable() { mockDatatableValidation(); - assertThrows(SQLInjectionException.class, () -> { + assertThrows(PlatformApiDataValidationException.class, () -> { underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1"); }); } @@ -83,7 +82,7 @@ public class ReadWriteNonCoreDataServiceImplTest { @Test public void testSqlInjectionCaughtQueryDataTable2() { mockDatatableValidation(); - assertThrows(SQLInjectionException.class, () -> { + assertThrows(PlatformApiDataValidationException.class, () -> { underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT"); }); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java index 52ca3180d..bec3c9a75 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java @@ -56,10 +56,8 @@ public class SavingsAccountTransactionDatatableIntegrationTest { private static final String SAVINGS_TRANSACTION_APP_TABLE_NAME = EntityTables.SAVINGS_TRANSACTION.getName(); public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; - public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy"; final String startDate = "01 Jun 2023"; final String firstDepositDate = "05 Jun 2023"; - final String secondDepositDate = "09 Jun 2023"; private RequestSpecification requestSpec; private ResponseSpecification responseSpec; private DatatableHelper datatableHelper; @@ -136,7 +134,7 @@ public class SavingsAccountTransactionDatatableIntegrationTest { assertNotNull(columnHeaderData); // two columns with 1 primary key and 2 audit columns created - assertEquals(columnHeaderData.size(), 6); + assertEquals(6, columnHeaderData.size()); // deleting the datatable String deletedDataTableName = this.datatableHelper.deleteDatatable(datatableName); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java index c89440375..4be682d15 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.integrationtests; +import static org.apache.fineract.infrastructure.core.service.DateUtils.parseLocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,18 +30,18 @@ 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.time.ZoneId; import java.util.HashMap; import java.util.List; -import org.apache.fineract.client.models.Filters; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem; -import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch; -import org.apache.fineract.client.models.RangeFilterBigDecimal; -import org.apache.fineract.client.models.RangeFilterLocalDate; import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; -import org.apache.fineract.client.models.SavingsTransactionSearch; -import org.apache.fineract.client.models.SortOrder; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; @@ -48,16 +49,21 @@ 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.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; @SuppressWarnings({ "rawtypes" }) public class SavingsAccountTransactionsSearchIntegrationTest { public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy"; + public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; final String startDate = "01 May 2023"; final String firstDepositDate = "05 May 2023"; final String secondDepositDate = "09 May 2023"; @@ -78,191 +84,229 @@ public class SavingsAccountTransactionsSearchIntegrationTest { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - this.responseSpecForValidationError = new ResponseSpecBuilder().expectStatusCode(400).build(); this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + this.responseSpecForValidationError = new ResponseSpecBuilder().expectStatusCode(400).build(); this.savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, this.responseSpecForValidationError); this.savingsProductHelper = new SavingsProductHelper(); } @Test - public void testSavingsTransactionsSearchWithAmountFilterLteGte() throws JsonProcessingException { + public void testSavingsTransactionsSearchAmountFrom() throws JsonProcessingException { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); - final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LTE, BigDecimal.valueOf(200))); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100)); + Map queryParams = buildTransactionsSearchQuery(searchParameters, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(1, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - BigDecimal expectedAmount = BigDecimal.valueOf(100); - assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); + assertEquals(2, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(2, pageItemsList.size()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(1).getAmount())); } @Test - public void testSavingsTransactionsSearchWithAmountFilterLtGt() throws JsonProcessingException { + public void testSavingsTransactionsSearchAmountFromTo() throws JsonProcessingException { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); - final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GT, BigDecimal.valueOf(100))); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(400))); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100)) + .toAmount(BigDecimal.valueOf(200)); + Map queryParams = buildTransactionsSearchQuery(searchParameters, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(1, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - BigDecimal expectedAmount = BigDecimal.valueOf(300); - assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); + assertEquals(1, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(1, pageItemsList.size()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(0).getAmount())); } @Test - public void testSavingsTransactionsSearchWithDateFilterLteGte() throws JsonProcessingException { + public void testSavingsTransactionsSearchDateFromTo() throws JsonProcessingException { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); - final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GTE, LocalDate.of(2023, 05, 06))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 05, 10))); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + TransactionSearchRequest searchParameters = new TransactionSearchRequest() + .fromDate(firstDepositDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE).toDate(withdrawDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(2, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(2, transactionsResponse.getPageItems().size()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - assertEquals(0, parseDate(withdrawDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); - assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + assertEquals(3, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(3, pageItemsList.size()); + assertEquals(parseLocalDate(withdrawDate, DEFAULT_DATE_FORMAT), pageItemsList.get(0).getDate()); + assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), pageItemsList.get(1).getDate()); } @Test - public void testSavingsTransactionsSearchWithTransactionTypeDepositAndDefaultSort() { + public void testSavingsTransactionsSearchSubmittedDateFromTo() throws JsonProcessingException { + LocalDate businessDate = LocalDate.now(ZoneId.systemDefault()); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, true); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); + + 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); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, false); + } + String submittedDate = DateUtils.format(businessDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + TransactionSearchRequest searchParameters = new TransactionSearchRequest() + .fromSubmittedDate(submittedDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE) + .toSubmittedDate(submittedDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + + Assertions.assertNotNull(transactionsResponse); + assertEquals(3, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(3, pageItemsList.size()); + assertEquals(businessDate, pageItemsList.get(0).getSubmittedOnDate()); + assertEquals(businessDate, pageItemsList.get(1).getSubmittedOnDate()); + assertEquals(businessDate, pageItemsList.get(2).getSubmittedOnDate()); + } + + @Test + public void testSavingsTransactionsSearchTransactionTypeDepositAndDefaultSort() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + int typeD = SavingsAccountTransactionType.DEPOSIT.getId(); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD)); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(2, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(2, transactionsResponse.getPageItems().size()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(0).getAmount())); - assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(1).getAmount())); - assertEquals(0, parseDate(firstDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + assertEquals(2, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(2, pageItemsList.size()); + GetSavingsAccountTransactionsPageItem first = pageItemsList.get(0); + assertEquals(Long.valueOf(typeD), first.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), first.getAmount())); + assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), first.getDate()); + GetSavingsAccountTransactionsPageItem second = pageItemsList.get(1); + assertEquals(Long.valueOf(typeD), second.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), second.getAmount())); + assertEquals(parseLocalDate(firstDepositDate, DEFAULT_DATE_FORMAT), second.getDate()); } @Test - public void testSavingsTransactionsSearchWithTransactionTypeWithdrawAndDeposit() throws JsonProcessingException { + public void testSavingsTransactionsSearchTransactionTypesWithdrawAndDeposit() throws JsonProcessingException { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); - final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT, Filters.TransactionTypeEnum.WITHDRAWAL)); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + int typeD = SavingsAccountTransactionType.DEPOSIT.getId(); + int typeW = SavingsAccountTransactionType.WITHDRAWAL.getId(); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD) + ',' + typeW); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(3, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(3, transactionsResponse.getPageItems().size()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - assertTrue( - Filters.TransactionTypeEnum.WITHDRAWAL.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(2).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(2).getAmount())); + assertEquals(3, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(3, pageItemsList.size()); + assertEquals(Long.valueOf(typeW), pageItemsList.get(0).getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(0).getAmount())); + assertEquals(Long.valueOf(typeD), pageItemsList.get(1).getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), pageItemsList.get(1).getAmount())); + assertEquals(Long.valueOf(typeD), pageItemsList.get(2).getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(2).getAmount())); } @Test - public void testSavingsTransactionsSearchWithPaginationAndNoFilter() { + public void testSavingsTransactionsSearchPaginationAndNoFilter() { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - int page = 0; - int size = 2; - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, null); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + + TransactionSearchRequest searchParameters = new TransactionSearchRequest().pageable(0, 2, null, null); + Map queryParams = buildTransactionsSearchQuery(searchParameters, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(3, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(2, transactionsResponse.getPageItems().size()); + assertEquals(3, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + assertEquals(2, transactionsResponse.getContent().size()); } @Test - public void testSavingsTransactionsSearchWithTransactionTypeDepositAndSortByAmountAsc() { + public void testSavingsTransactionsSearchTransactionTypeDepositAndSortByAmountAsc() { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "200", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); - SortOrder sortOrder = new SortOrder(); - sortOrder.setProperty("amount"); - sortOrder.setDirection(SortOrder.DirectionEnum.ASC); - List sortOrders = List.of(sortOrder); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, sortOrders); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); + + int typeD = SavingsAccountTransactionType.DEPOSIT.getId(); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD)).pageable(null, null, + "amount", Sort.Direction.ASC); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + Assertions.assertNotNull(transactionsResponse); - assertEquals(2, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(2, transactionsResponse.getPageItems().size()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); + assertEquals(2, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(2, pageItemsList.size()); + GetSavingsAccountTransactionsPageItem first = pageItemsList.get(0); + assertEquals(Long.valueOf(typeD), first.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), first.getAmount())); + assertEquals(parseLocalDate(firstDepositDate, DEFAULT_DATE_FORMAT), first.getDate()); + GetSavingsAccountTransactionsPageItem second = pageItemsList.get(1); + assertEquals(Long.valueOf(typeD), second.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), second.getAmount())); + assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), second.getDate()); } @Test @@ -270,69 +314,56 @@ public class SavingsAccountTransactionsSearchIntegrationTest { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); - this.savingsAccountHelper.postInterestForSavings(savingsId); - Filters filters = new Filters(); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 06))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 01))); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); - SortOrder sortOrder = new SortOrder(); - sortOrder.setProperty("amount"); - sortOrder.setDirection(SortOrder.DirectionEnum.DESC); - List sortOrders = List.of(sortOrder); - int page = 0; - int size = 2; - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders); - SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, - searchRequest); - Assertions.assertNotNull(transactionsResponse); - assertEquals(3, transactionsResponse.getTotalFilteredRecords()); - Assertions.assertNotNull(transactionsResponse.getPageItems()); - assertEquals(2, transactionsResponse.getPageItems().size()); - List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(400).compareTo(pageItemsList.get(0).getAmount())); - assertEquals(0, parseDate(thirdDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); - assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); - assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); - assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + int typeD = SavingsAccountTransactionType.DEPOSIT.getId(); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100)) + .toAmount(BigDecimal.valueOf(500)).fromDate("2023-05-06", DateUtils.DEFAULT_DATE_FORMAT, DEFAULT_LOCALE) + .toDate("2023-06-01", DateUtils.DEFAULT_DATE_FORMAT, DEFAULT_LOCALE).types(String.valueOf(typeD)) + .pageable(0, 2, "amount", Sort.Direction.DESC); + Map queryParams = buildTransactionsSearchQuery(searchParameters, DateUtils.DEFAULT_DATE_FORMAT, DEFAULT_LOCALE); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + + Assertions.assertNotNull(transactionsResponse); + assertEquals(3, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(2, pageItemsList.size()); + GetSavingsAccountTransactionsPageItem first = pageItemsList.get(0); + assertEquals(Long.valueOf(typeD), first.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(400), first.getAmount())); + assertEquals(parseLocalDate(thirdDepositDate, DEFAULT_DATE_FORMAT), first.getDate()); + GetSavingsAccountTransactionsPageItem second = pageItemsList.get(1); + assertEquals(Long.valueOf(typeD), second.getTransactionType().getId()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), second.getAmount())); + assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), second.getDate()); } @Test - public void testSavingsTransactionsSearchFilterRangeValidationError() { + public void testSavingsTransactionsSearchDateValidationError() { final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); Assertions.assertNotNull(clientID); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500))); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(1000))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 6))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 1))); - filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 8))); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); - SortOrder sortOrder = new SortOrder(); - sortOrder.setProperty("amount"); - sortOrder.setDirection(SortOrder.DirectionEnum.DESC); - List sortOrders = List.of(sortOrder); - int page = 0; - int size = 2; - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders); - this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest); + int typeD = SavingsAccountTransactionType.DEPOSIT.getId(); + TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100)) + .toAmount(BigDecimal.valueOf(500)); + Map queryParams = buildTransactionsSearchQuery(searchParameters, null, null); + queryParams.put("fromDate", "05 May 2023"); // wrong date format + this.savingsAccountHelperValidationError.searchSavingsTransactions(savingsId, queryParams); } @Test @@ -346,11 +377,11 @@ public class SavingsAccountTransactionsSearchIntegrationTest { this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); - Filters filters = new Filters(); - filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(-100))); - filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); - PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); - this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest); + TransactionSearchRequest searchParameters = new TransactionSearchRequest(); + Map queryParams = buildTransactionsSearchQuery(searchParameters, null, null); + queryParams.put("fromAmount", "test"); // not number + responseSpecForValidationError.statusCode(404); + this.savingsAccountHelperValidationError.searchSavingsTransactions(savingsId, queryParams); } private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { @@ -372,36 +403,53 @@ public class SavingsAccountTransactionsSearchIntegrationTest { return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } - private PagedRequestSavingsTransactionSearch buildTransactionsSearchReqeust(Filters filters, Integer page, Integer size, - List sorts) { - final Integer DEFAULT_PAGE_SIZE = 50; - SavingsTransactionSearch savingsTransactionSearch = new SavingsTransactionSearch(); - savingsTransactionSearch.setFilters(filters); - PagedRequestSavingsTransactionSearch pagedRequest = new PagedRequestSavingsTransactionSearch(); - pagedRequest.setRequest(savingsTransactionSearch); - pagedRequest.setSorts(sorts != null ? sorts : new ArrayList<>()); - pagedRequest.setPage(page != null ? page : 0); - pagedRequest.setSize(size != null ? size : DEFAULT_PAGE_SIZE); - return pagedRequest; - } - - private RangeFilterBigDecimal buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum operator, BigDecimal value) { - RangeFilterBigDecimal transactionAmountFilter = new RangeFilterBigDecimal(); - transactionAmountFilter.setOperator(operator); - transactionAmountFilter.setValue(value); - return transactionAmountFilter; - } - - private RangeFilterLocalDate buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum operator, LocalDate value) { - RangeFilterLocalDate transactionDateFilter = new RangeFilterLocalDate(); - transactionDateFilter.setOperator(operator); - transactionDateFilter.setValue(value); - return transactionDateFilter; - } - - public static LocalDate parseDate(String dateStr, String pattern) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); - return LocalDate.parse(dateStr, formatter); + private Map buildTransactionsSearchQuery(TransactionSearchRequest searchParams, String dateFormat, Locale locale) { + HashMap params = new HashMap<>(); + if (searchParams.getFromDate() != null) { + params.put("fromDate", DateUtils.format(searchParams.getFromDate(), dateFormat, locale)); + } + if (searchParams.getToDate() != null) { + params.put("toDate", DateUtils.format(searchParams.getToDate(), dateFormat, locale)); + } + if (searchParams.getFromSubmittedDate() != null) { + params.put("fromSubmittedDate", DateUtils.format(searchParams.getFromSubmittedDate(), dateFormat, locale)); + } + if (searchParams.getToSubmittedDate() != null) { + params.put("toSubmittedDate", DateUtils.format(searchParams.getToSubmittedDate(), dateFormat, locale)); + } + if (searchParams.getFromAmount() != null) { + params.put("fromAmount", searchParams.getFromAmount()); + } + if (searchParams.getToAmount() != null) { + params.put("toAmount", searchParams.getToAmount()); + } + if (searchParams.getTypes() != null) { + params.put("types", String.join(",", searchParams.getTypes())); + } + if (searchParams.getCredit() != null) { + params.put("credit", searchParams.getCredit()); + } + if (searchParams.getDebit() != null) { + params.put("debit", searchParams.getDebit()); + } + PageRequest pageable = searchParams.getPageable(); + if (pageable != null) { + params.put("offset", pageable.getPageNumber()); + params.put("limit", pageable.getPageSize()); + Sort sort = pageable.getSort(); + if (sort.isSorted()) { + List orders = sort.toList(); + params.put("sortOrder", orders.get(0).getDirection()); + params.put("orderBy", orders.stream().map(Sort.Order::getProperty).collect(Collectors.joining(","))); + } + } + if (dateFormat != null) { + params.put("dateFormat", dateFormat); + } + if (locale != null) { + params.put("locale", locale.toString()); + } + return params; } // Reset configuration fields diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index 7f6cc2d0f..48d0ccb6d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.integrationtests.common.savings; +import static org.apache.fineract.integrationtests.common.Utils.DEFAULT_TENANT; +import static org.apache.fineract.integrationtests.common.Utils.TENANT_PARAM_NAME; + import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import io.restassured.specification.RequestSpecification; @@ -36,9 +39,9 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch; import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -48,7 +51,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings({ "rawtypes" }) -public class SavingsAccountHelper { +public class SavingsAccountHelper extends IntegrationTest { private final RequestSpecification requestSpec; private final ResponseSpecification responseSpec; @@ -650,12 +653,11 @@ public class SavingsAccountHelper { return Utils.performServerGet(requestSpec, responseSpec, URL, ""); } - public SavingsAccountTransactionsSearchResponse searchTransactions(Integer savingsId, - PagedRequestSavingsTransactionSearch searchReqeust) { - final String SAVINGS_TRANSACTIONS_SEARCH_URL = SAVINGS_ACCOUNT_URL + "/" + savingsId + "/transactions/search" + "?" - + Utils.TENANT_IDENTIFIER; - String jsonBodyToSend = GSON.toJson(searchReqeust); - String response = Utils.performServerPost(this.requestSpec, this.responseSpec, SAVINGS_TRANSACTIONS_SEARCH_URL, jsonBodyToSend); + public SavingsAccountTransactionsSearchResponse searchSavingsTransactions(Integer savingsId, Map queryParams) { + final String url = SAVINGS_ACCOUNT_URL + "/" + savingsId + "/transactions/search"; + queryParams.put(TENANT_PARAM_NAME, DEFAULT_TENANT); + requestSpec.queryParams(queryParams); + String response = Utils.performServerGet(this.requestSpec, this.responseSpec, url); return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class); }