FINERACT-1943 Transaction and datatable GET queries

This commit is contained in:
jmarta
2023-08-25 10:56:16 +02:00
committed by Adam Saghy
parent ee31ff0ed8
commit e20e715488
29 changed files with 794 additions and 911 deletions
@@ -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;
}
@@ -25,7 +25,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.Data; import lombok.Data;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@Data @Data
@@ -44,7 +43,7 @@ public class PagedRequest<T> {
return Optional.ofNullable(request); return Optional.ofNullable(request);
} }
public Pageable toPageable() { public PageRequest toPageable() {
if (isEmpty(sorts)) { if (isEmpty(sorts)) {
return PageRequest.of(page, size); return PageRequest.of(page, size);
} else { } else {
@@ -57,16 +56,11 @@ public class PagedRequest<T> {
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
private static class SortOrder { private static class SortOrder {
private Direction direction; private Sort.Direction direction;
private String property; private String property;
private enum Direction {
ASC, DESC;
}
private Sort.Order toOrder() { private Sort.Order toOrder() {
Sort.Direction d = Sort.Direction.fromString(direction.name()); return new Sort.Order(direction, property);
return new Sort.Order(d, property);
} }
} }
} }
@@ -41,11 +41,14 @@ public class DatabaseSpecificSQLGenerator {
this.databaseTypeResolver = databaseTypeResolver; this.databaseTypeResolver = databaseTypeResolver;
} }
public DatabaseType getDialect() {
return databaseTypeResolver.databaseType();
}
public String escape(String arg) { public String escape(String arg) {
DatabaseType dialect = databaseTypeResolver.databaseType(); if (databaseTypeResolver.isMySQL()) {
if (dialect.isMySql()) {
return format("`%s`", arg); return format("`%s`", arg);
} else if (dialect.isPostgres()) { } else if (databaseTypeResolver.isPostgreSQL()) {
return format("\"%s\"", arg); return format("\"%s\"", arg);
} }
return arg; return arg;
@@ -16,14 +16,23 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * 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.Data;
import lombok.NoArgsConstructor;
/**
* Immutable data object representing datatable data.
*/
@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<String, Object> parametersMap;
} }
@@ -99,6 +99,10 @@ public enum SavingsAccountTransactionType {
return transactionType == null ? INVALID : transactionType; return transactionType == null ? INVALID : transactionType;
} }
public boolean isValid() {
return this != INVALID;
}
public boolean isDeposit() { public boolean isDeposit() {
return this == DEPOSIT; return this == DEPOSIT;
} }
@@ -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.SearchParameters;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; 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.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.DatatableCheckStatusData;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableChecksData; import org.apache.fineract.infrastructure.dataqueries.data.DatatableChecksData;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; 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 return this.jdbcTemplate.query(sql, this.registerDataTableMapper); // NOSONAR
} }
protected static final class RegisterDataTableMapper implements RowMapper<DatatableChecksData> { protected final class RegisterDataTableMapper implements RowMapper<DatatableChecksData> {
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 @Override
public DatatableChecksData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { public DatatableChecksData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final String entity = rs.getString("entity"); final String entity = rs.getString("entity");
final String tableName = rs.getString("tableName"); final String tableName = rs.getString("tableName");
@@ -184,8 +187,8 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat
} }
public String schema() { public String schema() {
return " t.application_table_name as entity, t.registered_table_name as tableName " + " from x_registered_table t " String[] values = EntityTables.getFiltered(EntityTables::hasCheck).stream().map(EntityTables::getName).toArray(String[]::new);
+ " where application_table_name IN( 'm_client','m_group','m_savings_account','m_loan')"; return SELECT_FROM + SqlOperator.IN.formatSql(sqlGenerator, JdbcJavaType.VARCHAR, "application_table_name", null, values);
} }
} }
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.dataqueries.service; package org.apache.fineract.infrastructure.dataqueries.service;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import jakarta.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -32,8 +33,6 @@ public interface ReadWriteNonCoreDataService {
DatatableData retrieveDatatable(String datatable); DatatableData retrieveDatatable(String datatable);
List<JsonObject> queryDataTable(String datatable, String columnFilter, String valueFilter, String resultColumns);
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')") @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')")
void registerDatatable(JsonCommand command); void registerDatatable(JsonCommand command);
@@ -73,4 +72,7 @@ public interface ReadWriteNonCoreDataService {
String getDataTableName(String Url); String getDataTableName(String Url);
Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn); Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn);
List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue,
@NotNull String resultColumns);
} }
@@ -210,8 +210,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
throw new PlatformApiDataValidationException(errors); throw new PlatformApiDataValidationException(errors);
} }
DatabaseType dialect = databaseTypeResolver.databaseType(); Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, false, sqlGenerator);
Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, dialect);
String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE "
+ EQ.formatPlaceholder(sqlGenerator, columnName, null); + EQ.formatPlaceholder(sqlGenerator, columnName, null);
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue);
@@ -1812,7 +1811,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
String queryParamColumnUnderscored; String queryParamColumnUnderscored;
String columnHeaderUnderscored; String columnHeaderUnderscored;
boolean notFound; boolean notFound;
DatabaseType dialect = databaseTypeResolver.databaseType();
final Map<String, Object> affectedColumns = new HashMap<>(); final Map<String, Object> affectedColumns = new HashMap<>();
final Set<String> keys = queryParams.keySet(); final Set<String> keys = queryParams.keySet();
@@ -1833,7 +1831,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) { if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) {
pValue = queryParams.get(key); pValue = queryParams.get(key);
validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat, validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat,
clientApplicationLocale, dialect); clientApplicationLocale, true, sqlGenerator);
affectedColumns.put(columnHeader.getColumnName(), validatedValue); affectedColumns.put(columnHeader.getColumnName(), validatedValue);
notFound = false; notFound = false;
} }
@@ -37,7 +37,9 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.math.BigDecimal;
import java.util.Collection; import java.util.Collection;
import java.util.Locale;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper; 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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.PagedRequest;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; 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.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.dao.CannotAcquireLockException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -76,7 +79,7 @@ public class SavingsAccountTransactionsApiResource {
private final ApiRequestParameterHelper apiRequestParameterHelper; private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final PaymentTypeReadPlatformService paymentTypeReadPlatformService; private final PaymentTypeReadPlatformService paymentTypeReadPlatformService;
private final SavingsAccountTransactionsSearchServiceImpl transactionsSearchServiceImpl; private final SavingsAccountTransactionSearchService transactionsSearchService;
private boolean is(final String commandParam, final String commandValue) { private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
@@ -124,16 +127,35 @@ public class SavingsAccountTransactionsApiResource {
SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS); SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS);
} }
@POST @GET
@Path("search") @Path("search")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Search Savings Account Transactions") @Operation(summary = "Search Savings Account Transactions")
@ApiResponses({ @ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class))) }) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class))) })
public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savingsId") final Long savingsId, public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savings account id") final Long savingsId,
@Parameter PagedRequest<SavingsTransactionSearch> searchRequest) { @QueryParam("fromDate") @Parameter(description = "minimum value date (inclusive)", example = "2023-08-08") final String fromDate,
Page<SavingsAccountTransactionData> transactionsData = transactionsSearchServiceImpl.searchTransactions(savingsId, searchRequest); @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<SavingsAccountTransactionData> transactionsData = transactionsSearchService.searchTransactions(savingsId, searchParameters);
return toApiJsonSerializer.serialize(transactionsData); return toApiJsonSerializer.serialize(transactionsData);
} }
@@ -185,8 +185,8 @@ final class SavingsAccountTransactionsApiResourceSwagger {
} }
@Schema(example = "2") @Schema(example = "2")
public Long totalFilteredRecords; public Long total;
public Set<GetSavingsAccountTransactionsPageItem> pageItems; public Set<GetSavingsAccountTransactionsPageItem> content;
} }
@Schema(description = "PostSavingsAccountTransactionsRequest") @Schema(description = "PostSavingsAccountTransactionsRequest")
@@ -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<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME);
if (searchFilters != null) {
List<RangeFilter<LocalDate>> dateFilters = searchFilters.getTransactionDate();
validateRangeFilters(baseDataValidator, dateFilters, transactionDateParamName);
List<RangeFilter<BigDecimal>> amountFilters = searchFilters.getTransactionAmount();
validateRangeFilters(baseDataValidator, amountFilters, transactionAmountParamName);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private <T> void validateRangeFilters(DataValidatorBuilder baseDataValidator, List<RangeFilter<T>> rangeFilters, String paramName) {
if (rangeFilters == null) {
return;
}
if (rangeFilters.size() > 2) {
baseDataValidator.parameter(paramName).value(rangeFilters).notExceedingListLengthOf(2);
}
if (!rangeFilters.isEmpty()) {
RangeFilter<T> firstFilter = rangeFilters.get(0);
RangeOperator firstOperator = firstFilter.getOperator();
if (rangeFilters.size() == 2) {
RangeFilter<T> secondFilter = rangeFilters.get(1);
RangeOperator secondOperator = secondFilter.getOperator();
if (((firstOperator == RangeOperator.GT || firstOperator == RangeOperator.GTE)
&& !(secondOperator == RangeOperator.LT || secondOperator == RangeOperator.LTE))
|| ((firstOperator == RangeOperator.LT || firstOperator == RangeOperator.LTE)
&& !(secondOperator == RangeOperator.GT || secondOperator == RangeOperator.GTE))) {
baseDataValidator.parameter(paramName).failWithCode("invalid.range", firstOperator, secondOperator);
}
}
}
for (RangeFilter<T> filter : rangeFilters) {
T value = filter.getValue();
if (value instanceof BigDecimal) {
baseDataValidator.parameter(paramName).value(value).zeroOrPositiveAmount();
}
}
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -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);
}
}
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.LockModeType; import jakarta.persistence.LockModeType;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionsSearchRepository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 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.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
public interface SavingsAccountTransactionRepository extends JpaRepository<SavingsAccountTransaction, Long>, public interface SavingsAccountTransactionRepository
JpaSpecificationExecutor<SavingsAccountTransaction>, SavingsTransactionsSearchRepository { extends JpaRepository<SavingsAccountTransaction, Long>, JpaSpecificationExecutor<SavingsAccountTransaction> {
@Query("select sat from SavingsAccountTransaction sat where sat.id = :transactionId and sat.savingsAccount.id = :savingsId") @Query("select sat from SavingsAccountTransaction sat where sat.id = :transactionId and sat.savingsAccount.id = :savingsId")
SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId, SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId,
@@ -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<RangeFilter<LocalDate>> transactionDate;
private List<RangeFilter<BigDecimal>> transactionAmount;
private List<SavingsAccountTransactionType> transactionType;
}
@Data
public static class RangeFilter<T> {
private RangeOperator operator;
private T value;
}
}
@@ -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;
}
@@ -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<SavingsTransactionSearchResult> searchTransactions(SavingsTransactionSearchParameters searchParameters);
}
@@ -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<SavingsTransactionSearchResult> searchTransactions(SavingsTransactionSearchParameters searchParameters) {
// Build base query with filters but without the selection
BaseQueryParametersMapResult baseQueryParameterMapResult = buildBaseQueryWithFilters(searchParameters.getSavingsId(),
searchParameters.getDepositAccountType(), searchParameters.getFilters());
// Attach the selection
String jpqlQuery = attachSelection(baseQueryParameterMapResult.getBaseQueryString());
// Attach the ordering
String queryWithOrdering = attachOrdering(jpqlQuery, searchParameters.getPageable().getSort());
// Execute Query
TypedQuery<SavingsTransactionSearchResult> queryToExecute = entityManager.createQuery(queryWithOrdering,
SavingsTransactionSearchResult.class);
setQueryParameters(queryToExecute, baseQueryParameterMapResult.getParametersMap());
applyPagination(queryToExecute, searchParameters.getPageable());
List<SavingsTransactionSearchResult> resultList = queryToExecute.getResultList();
// Attach the count selection
String countQuery = attachCountSelection(baseQueryParameterMapResult.getBaseQueryString());
// Execute count query
TypedQuery<Long> countQueryToExecute = entityManager.createQuery(countQuery, Long.class);
setQueryParameters(countQueryToExecute, baseQueryParameterMapResult.getParametersMap());
Long totalElements = countQueryToExecute.getSingleResult();
return PageableExecutionUtils.getPage(resultList, searchParameters.getPageable(), () -> totalElements);
}
private <T> void setQueryParameters(TypedQuery<T> queryToExecute, Map<String, Object> parametersMap) {
for (Map.Entry<String, Object> entry : parametersMap.entrySet()) {
queryToExecute.setParameter(entry.getKey(), entry.getValue());
}
}
private BaseQueryParametersMapResult buildBaseQueryWithFilters(Long savingsId, DepositAccountType depositAccountType, Filters filters) {
String baseQuery = """
SELECT tr
FROM SavingsAccountTransaction tr
JOIN ApplicationCurrency currency ON (currency.code = tr.savingsAccount.currency.code)
LEFT JOIN AccountTransferTransaction fromtran ON (fromtran.fromSavingsTransaction = tr)
LEFT JOIN AccountTransferTransaction totran ON (totran.toSavingsTransaction = tr)
LEFT JOIN tr.notes nt ON (nt.savingsTransaction = tr)
WHERE tr.savingsAccount.id = :savingsId
AND tr.savingsAccount.depositType = :depositType
""";
StringBuilder baseQueryBuilder = new StringBuilder(baseQuery);
Map<String, Object> parameterMap = new HashMap<>();
parameterMap.put("savingsId", savingsId);
parameterMap.put("depositType", depositAccountType.getValue());
setFilterConditions(baseQueryBuilder, parameterMap, filters);
return new BaseQueryParametersMapResult(baseQueryBuilder.toString(), parameterMap);
}
private String attachSelection(String baseQuery) {
return baseQuery.replace("SELECT tr",
"SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult(tr.id,tr.typeOf, tr.dateOf, tr.amount, tr.releaseIdOfHoldAmountTransaction, tr.reasonForBlock,tr.createdDate, tr.appUser, nt.note, tr.runningBalance, tr.reversed,tr.reversalTransaction, tr.originalTxnId, tr.lienTransaction, tr.isManualTransaction,fromTran, toTran, tr.savingsAccount, tr.paymentDetail, currency) ");
}
private String attachOrdering(String jpqlQuery, Sort sort) {
StringJoiner orderByClauseBuilder = new StringJoiner(", ", " ORDER BY ", "");
if (Objects.nonNull(sort) && sort.isSorted()) {
buildOrderByClause(sort.toList(), orderByClauseBuilder);
} else {
List<Order> defaultOrders = getDefaultOrders();
buildOrderByClause(defaultOrders, orderByClauseBuilder);
}
return new StringBuilder(jpqlQuery).append(orderByClauseBuilder.toString()).toString();
}
private void buildOrderByClause(List<Order> orders, StringJoiner orderByClauseBuilder) {
for (Order order : orders) {
String property = "tr." + order.getProperty();
String direction = order.getDirection().name();
String orderByExpression = new StringBuilder(property).append(StringUtils.SPACE).append(direction).toString();
orderByClauseBuilder.add(orderByExpression);
}
}
private List<Order> getDefaultOrders() {
return List.of(Order.desc(TRANSACTION_DATE_FIELD_NAME), Order.desc(CREATED_DATE_FIELD_NAME), Order.desc(ID_FIELD_NAME));
}
private void applyPagination(TypedQuery<?> query, Pageable pageable) {
if (pageable.isPaged()) {
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
}
}
private String attachCountSelection(String baseQuery) {
return baseQuery.replace("SELECT tr", "SELECT COUNT(tr) ");
}
private void setFilterConditions(StringBuilder queryBuilder, Map<String, Object> parameterMap, Filters filters) {
if (Objects.nonNull(filters)) {
List<RangeFilter<LocalDate>> dateFilters = filters.getTransactionDate();
List<RangeFilter<BigDecimal>> amountFilters = filters.getTransactionAmount();
List<SavingsAccountTransactionType> transactionTypes = filters.getTransactionType();
if (Objects.nonNull(dateFilters)) {
processRangeFilters(queryBuilder, parameterMap, dateFilters, TRANSACTION_DATE_FIELD_NAME);
}
if (Objects.nonNull(amountFilters)) {
processRangeFilters(queryBuilder, parameterMap, amountFilters, AMOUNT_FIELD_NAME);
}
if (CollectionUtils.isNotEmpty(transactionTypes)) {
List<Integer> transactionTypeValues = transactionTypes.stream().map(SavingsAccountTransactionType::getValue)
.collect(Collectors.toList());
queryBuilder.append(" AND tr.typeOf IN :transactionTypes ");
parameterMap.put("transactionTypes", transactionTypeValues);
}
}
}
private <T> void processRangeFilters(StringBuilder queryBuilder, Map<String, Object> parameterMap, List<RangeFilter<T>> filters,
String field) {
filters.forEach(filter -> {
String paramName = new StringBuilder(field).append(filter.getOperator()).toString();
queryBuilder.append(" AND tr.").append(field).append(StringUtils.SPACE).append(filter.getOperator().getSymbol()).append(" :")
.append(paramName);
parameterMap.put(paramName, filter.getValue());
});
}
}
@@ -1313,52 +1313,54 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
* return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); } * return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); }
*/ */
private static final class SavingsAccountTransactionsMapper implements RowMapper<SavingsAccountTransactionData> { public static final class SavingsAccountTransactionsMapper implements RowMapper<SavingsAccountTransactionData> {
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); private static String buildSelect() {
sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); return "tr.id as transactionId, tr.transaction_type_enum as transactionType, "
sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); + "tr.transaction_date as transactionDate, tr.amount as transactionAmount, "
sqlBuilder.append(" tr.release_id_of_hold_amount as releaseTransactionId,"); + "tr.release_id_of_hold_amount as releaseTransactionId, tr.reason_for_block as reasonForBlock, "
sqlBuilder.append(" tr.reason_for_block as reasonForBlock,"); + "tr.submitted_on_date as submittedOnDate, au.username as submittedByUsername, nt.note as transactionNote, "
sqlBuilder.append("tr.submitted_on_date as submittedOnDate,"); + "tr.running_balance_derived as runningBalance, tr.is_reversed as reversed, "
sqlBuilder.append(" au.username as submittedByUsername, "); + "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, "
sqlBuilder.append(" nt.note as transactionNote, "); + "fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed, "
sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + "fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount, "
sqlBuilder.append( + "fromtran.description as fromTransferDescription, "
"tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, "); + "totran.id as toTransferId, totran.is_reversed as toTransferReversed, "
sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,"); + "totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, "
sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"); + "totran.description as toTransferDescription, sa.id as savingsId, sa.account_no as accountNo, "
sqlBuilder.append("fromtran.description as fromTransferDescription,"); + "pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "
sqlBuilder.append("totran.id as toTransferId, totran.is_reversed as toTransferReversed,"); + "pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "
sqlBuilder.append("totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,"); + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "
sqlBuilder.append("totran.description as toTransferDescription,"); + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "
sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,"); + "curr.display_symbol as currencyDisplaySymbol, pt.value as paymentTypeName, " + "tr.is_manual as postInterestAsOn ";
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( private static String buildFrom() {
"sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); return " FROM m_savings_account_transaction tr join m_savings_account sa on tr.savings_account_id = sa.id "
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + "join m_currency curr on curr.code = sa.currency_code "
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); + "left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id "
sqlBuilder.append("pt.value as paymentTypeName, "); + "left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id "
sqlBuilder.append("tr.is_manual as postInterestAsOn "); + "left join m_payment_detail pd on tr.payment_detail_id = pd.id "
sqlBuilder.append("from m_savings_account sa "); + "left join m_payment_type pt on pd.payment_type_id = pt.id left join m_appuser au on au.id=tr.appuser_id "
sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id "); + "left join m_note nt ON nt.savings_account_transaction_id=tr.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();
} }
public String schema() { public String schema() {
return this.schemaSql; return SCHEMA;
}
public String select() {
return SELECT;
}
public String from() {
return FROM;
} }
@Override @Override
@@ -18,13 +18,12 @@
*/ */
package org.apache.fineract.portfolio.savings.service.search; package org.apache.fineract.portfolio.savings.service.search;
import org.apache.fineract.infrastructure.core.service.Page; import jakarta.validation.constraints.NotNull;
import org.apache.fineract.infrastructure.core.service.PagedRequest;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; 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 { public interface SavingsAccountTransactionSearchService {
Page<SavingsAccountTransactionData> searchTransactions(Long savingsId, PagedRequest<SavingsTransactionSearch> searchRequest); Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters);
} }
@@ -18,21 +18,37 @@
*/ */
package org.apache.fineract.portfolio.savings.service.search; package org.apache.fineract.portfolio.savings.service.search;
import java.util.Objects; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
import java.util.Optional;
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 lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.service.PagedRequest; 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.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.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionSearchValidator; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; import org.apache.fineract.portfolio.search.data.ColumnFilterData;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; import org.apache.fineract.portfolio.search.service.SearchUtil;
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; import org.jetbrains.annotations.Nullable;
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearchParameters; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -42,35 +58,117 @@ import org.springframework.transaction.annotation.Transactional;
public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccountTransactionSearchService { public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccountTransactionSearchService {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final GenericDataService genericDataService;
private final SavingsAccountTransactionRepository savingsTransactionRepository; private final DatabaseSpecificSQLGenerator sqlGenerator;
private final JdbcTemplate jdbcTemplate;
private final SavingsAccountTransactionSearchValidator searchValidator;
@Override @Override
public Page<SavingsAccountTransactionData> searchTransactions(Long savingsId, PagedRequest<SavingsTransactionSearch> searchRequest) { public Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId,
validateSearchRequest(searchRequest); @NotNull TransactionSearchRequest searchParameters) {
return executeSearch(savingsId, DepositAccountType.SAVINGS_DEPOSIT, searchRequest); context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME);
String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName();
Map<String, ResultsetColumnHeaderData> columnHeaders = SearchUtil
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(apptable));
PageRequest pageable = searchParameters.getPageable();
PageRequest sortPageable;
if (pageable.getSort().isSorted()) {
List<ApiParameterError> errors = new ArrayList<>();
List<Sort.Order> 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<ColumnFilterData> 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<SavingsAccountTransactionData> emptyResult = PageableExecutionUtils.getPage(new ArrayList<>(0), pageable, () -> 0);
if (addTransactionTypesFilter(searchParameters, columnFilters) == null) {
return emptyResult;
}
String alias = "tr";
StringBuilder where = new StringBuilder(" WHERE ");
ArrayList<Object> 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<SavingsAccountTransactionData> results = this.jdbcTemplate.query(query.toString(), tm, args);
return PageableExecutionUtils.getPage(results, pageable, () -> totalElements);
} }
private void validateSearchRequest(PagedRequest<SavingsTransactionSearch> searchRequest) { private static void addFromToFilter(@NotNull String column, String fromValue, String toValue,
Objects.requireNonNull(searchRequest, "searchRequest must not be null"); @NotNull List<ColumnFilterData> columnFilters) {
context.isAuthenticated(); if (fromValue != null) {
Optional<SavingsTransactionSearch> request = searchRequest.getRequest(); columnFilters.add(toValue == null ? ColumnFilterData.create(column, SqlOperator.GTE, fromValue)
Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); : ColumnFilterData.btw(column, fromValue, toValue));
searchValidator.validateSearchFilters(searchFilters); } else if (toValue != null) {
columnFilters.add(ColumnFilterData.create(column, SqlOperator.LTE, toValue));
}
} }
private Page<SavingsAccountTransactionData> executeSearch(Long savingsId, DepositAccountType depositType, @Nullable
PagedRequest<SavingsTransactionSearch> searchRequest) { private static Boolean addTransactionTypesFilter(@NotNull TransactionSearchRequest searchParameters,
Optional<SavingsTransactionSearch> request = searchRequest.getRequest(); List<ColumnFilterData> columnFilters) {
Pageable pageable = searchRequest.toPageable(); Predicate<SavingsAccountTransactionType> filter = null;
Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); Boolean credit = searchParameters.getCredit();
SavingsTransactionSearchParameters searchParameters = SavingsTransactionSearchParameters.builder().savingsId(savingsId) Boolean debit = searchParameters.getDebit();
.depositAccountType(DepositAccountType.SAVINGS_DEPOSIT).filters(searchFilters).pageable(pageable).build();
org.springframework.data.domain.Page<SavingsAccountTransactionData> pageResult = savingsTransactionRepository
.searchTransactions(searchParameters).map(SavingsTransactionSearchResult::toSavingsAccountTransactionData);
return new Page<>(pageResult.getContent(), Long.valueOf(pageResult.getTotalElements()).intValue());
}
if (credit != null) {
Predicate<SavingsAccountTransactionType> cf = SavingsAccountTransactionType::isCreditEntryType;
filter = credit ? cf : Predicate.not(cf);
}
if (debit != null) {
Predicate<SavingsAccountTransactionType> 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<String> types = Arrays.asList(searchParameters.getTypes());
Predicate<SavingsAccountTransactionType> tf = t -> types.contains(String.valueOf(t.getId()));
filter = filter == null ? tf : filter.and(tf);
}
if (filter != null) {
filter = filter.and(SavingsAccountTransactionType::isValid);
List<SavingsAccountTransactionType> 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;
}
} }
@@ -23,7 +23,14 @@ import java.util.Set;
public final class SearchConstants { 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_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() {} private SearchConstants() {}
@@ -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<FilterData> 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)));
}
}
@@ -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<String> 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));
}
}
@@ -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;
}
}
@@ -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.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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.JdbcJavaType;
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator; 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; import org.springframework.jdbc.support.rowset.SqlRowSet;
public final class SearchUtil { public final class SearchUtil {
@@ -112,6 +115,7 @@ public final class SearchUtil {
} }
} }
@NotNull
public static List<String> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> columnHeaders, public static List<String> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> columnHeaders,
List<ApiParameterError> errors, boolean allowEmpty) { List<ApiParameterError> errors, boolean allowEmpty) {
List<String> result = new ArrayList<>(); List<String> 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)); errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
} }
if (column != null) { if (column != null) {
SQLInjectionValidator.validateDynamicQuery(column);
if (!columnHeaders.containsKey(column)) { if (!columnHeaders.containsKey(column)) {
column = camelToSnake(column); column = camelToSnake(column);
if (!columnHeaders.containsKey(column)) { if (!columnHeaders.containsKey(column)) {
@@ -143,8 +146,64 @@ public final class SearchUtil {
return column; return column;
} }
public static boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where,
@NotNull List<Object> params, String alias, Map<String, ResultsetColumnHeaderData> 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<Object> params,
String alias, Map<String, ResultsetColumnHeaderData> columnHeaders, boolean embedded, boolean strict,
@NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
String columnName = columnFilter.getColumn();
List<FilterData> 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<String> values = filter.getValues();
List<Object> 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, public static Object parseAndValidateJdbcColumnValue(String column, String columnValue,
Map<String, ResultsetColumnHeaderData> columnHeaders, DatabaseType dialect) { Map<String, ResultsetColumnHeaderData> columnHeaders, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
List<ApiParameterError> errors = new ArrayList<>(); List<ApiParameterError> errors = new ArrayList<>();
column = validateToJdbcColumn(column, columnHeaders, errors, false); column = validateToJdbcColumn(column, columnHeaders, errors, false);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@@ -152,12 +211,12 @@ public final class SearchUtil {
} }
ResultsetColumnHeaderData columnHeader = columnHeaders.get(column); ResultsetColumnHeaderData columnHeader = columnHeaders.get(column);
String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT; String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT;
return columnHeader.getColumnType().toJdbcValue(dialect, return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(),
parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, dialect), false); parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, strict, sqlGenerator), false);
} }
public static Object parseAndValidateColumnValue(final ResultsetColumnHeaderData columnHeader, final String pValue, 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; String columnValue = pValue;
JdbcJavaType colType = columnHeader.getColumnType(); JdbcJavaType colType = columnHeader.getColumnType();
if (!colType.isStringType() || !columnHeader.isMandatory()) { if (!colType.isStringType() || !columnHeader.isMandatory()) {
@@ -173,7 +232,9 @@ public final class SearchUtil {
if (StringUtils.isEmpty(columnValue)) { if (StringUtils.isEmpty(columnValue)) {
return columnValue; return columnValue;
} }
SQLInjectionValidator.validateDynamicQuery(columnValue); if (strict) {
SQLInjectionValidator.validateDynamicQuery(columnValue);
}
if (columnHeader.hasColumnValues()) { if (columnHeader.hasColumnValues()) {
if (columnHeader.isCodeValueDisplayType()) { if (columnHeader.isCodeValueDisplayType()) {
@@ -193,7 +254,7 @@ public final class SearchUtil {
return codeLookup; return codeLookup;
} else { } else {
throw new PlatformDataIntegrityException("error.msg.invalid.columnType.", "Code: " + columnHeader.getColumnName() 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)");
} }
} }
@@ -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.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@@ -75,7 +74,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
@Test @Test
public void testSqlInjectionCaughtQueryDataTable() { public void testSqlInjectionCaughtQueryDataTable() {
mockDatatableValidation(); mockDatatableValidation();
assertThrows(SQLInjectionException.class, () -> { assertThrows(PlatformApiDataValidationException.class, () -> {
underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1"); underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1");
}); });
} }
@@ -83,7 +82,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
@Test @Test
public void testSqlInjectionCaughtQueryDataTable2() { public void testSqlInjectionCaughtQueryDataTable2() {
mockDatatableValidation(); mockDatatableValidation();
assertThrows(SQLInjectionException.class, () -> { assertThrows(PlatformApiDataValidationException.class, () -> {
underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT"); underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT");
}); });
} }
@@ -56,10 +56,8 @@ public class SavingsAccountTransactionDatatableIntegrationTest {
private static final String SAVINGS_TRANSACTION_APP_TABLE_NAME = EntityTables.SAVINGS_TRANSACTION.getName(); 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 ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy";
final String startDate = "01 Jun 2023"; final String startDate = "01 Jun 2023";
final String firstDepositDate = "05 Jun 2023"; final String firstDepositDate = "05 Jun 2023";
final String secondDepositDate = "09 Jun 2023";
private RequestSpecification requestSpec; private RequestSpecification requestSpec;
private ResponseSpecification responseSpec; private ResponseSpecification responseSpec;
private DatatableHelper datatableHelper; private DatatableHelper datatableHelper;
@@ -136,7 +134,7 @@ public class SavingsAccountTransactionDatatableIntegrationTest {
assertNotNull(columnHeaderData); assertNotNull(columnHeaderData);
// two columns with 1 primary key and 2 audit columns created // two columns with 1 primary key and 2 audit columns created
assertEquals(columnHeaderData.size(), 6); assertEquals(6, columnHeaderData.size());
// deleting the datatable // deleting the datatable
String deletedDataTableName = this.datatableHelper.deleteDatatable(datatableName); String deletedDataTableName = this.datatableHelper.deleteDatatable(datatableName);
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.integrationtests; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,18 +30,18 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; 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.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.SavingsAccountTransactionsSearchResponse;
import org.apache.fineract.client.models.SavingsTransactionSearch; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.client.models.SortOrder; 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.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; 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.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; 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.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@SuppressWarnings({ "rawtypes" }) @SuppressWarnings({ "rawtypes" })
public class SavingsAccountTransactionsSearchIntegrationTest { public class SavingsAccountTransactionsSearchIntegrationTest {
public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy"; 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 startDate = "01 May 2023";
final String firstDepositDate = "05 May 2023"; final String firstDepositDate = "05 May 2023";
final String secondDepositDate = "09 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 = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.responseSpecForValidationError = new ResponseSpecBuilder().expectStatusCode(400).build();
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
this.responseSpecForValidationError = new ResponseSpecBuilder().expectStatusCode(400).build();
this.savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, this.responseSpecForValidationError); this.savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, this.responseSpecForValidationError);
this.savingsProductHelper = new SavingsProductHelper(); this.savingsProductHelper = new SavingsProductHelper();
} }
@Test @Test
public void testSavingsTransactionsSearchWithAmountFilterLteGte() throws JsonProcessingException { public void testSavingsTransactionsSearchAmountFrom() throws JsonProcessingException {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100));
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, null, null);
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LTE, BigDecimal.valueOf(200))); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); queryParams);
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId,
searchRequest);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(1, transactionsResponse.getTotalFilteredRecords()); assertEquals(2, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
BigDecimal expectedAmount = BigDecimal.valueOf(100); assertEquals(2, pageItemsList.size());
assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(1).getAmount()));
} }
@Test @Test
public void testSavingsTransactionsSearchWithAmountFilterLtGt() throws JsonProcessingException { public void testSavingsTransactionsSearchAmountFromTo() throws JsonProcessingException {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100))
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GT, BigDecimal.valueOf(100))); .toAmount(BigDecimal.valueOf(200));
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(400))); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, null, null);
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, queryParams);
searchRequest);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(1, transactionsResponse.getTotalFilteredRecords()); assertEquals(1, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
BigDecimal expectedAmount = BigDecimal.valueOf(300); assertEquals(1, pageItemsList.size());
assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(0).getAmount()));
} }
@Test @Test
public void testSavingsTransactionsSearchWithDateFilterLteGte() throws JsonProcessingException { public void testSavingsTransactionsSearchDateFromTo() throws JsonProcessingException {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); TransactionSearchRequest searchParameters = new TransactionSearchRequest()
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GTE, LocalDate.of(2023, 05, 06))); .fromDate(firstDepositDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE).toDate(withdrawDate, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE);
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 05, 10))); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE);
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, queryParams);
searchRequest);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(2, transactionsResponse.getTotalFilteredRecords()); assertEquals(3, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
assertEquals(2, transactionsResponse.getPageItems().size()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); assertEquals(3, pageItemsList.size());
assertEquals(0, parseDate(withdrawDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); assertEquals(parseLocalDate(withdrawDate, DEFAULT_DATE_FORMAT), pageItemsList.get(0).getDate());
assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), pageItemsList.get(1).getDate());
} }
@Test @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); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); 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<String, Object> 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<GetSavingsAccountTransactionsPageItem> 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); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); int typeD = SavingsAccountTransactionType.DEPOSIT.getId();
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD));
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE);
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
searchRequest); queryParams);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(2, transactionsResponse.getTotalFilteredRecords()); assertEquals(2, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
assertEquals(2, transactionsResponse.getPageItems().size()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); assertEquals(2, pageItemsList.size());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); GetSavingsAccountTransactionsPageItem first = pageItemsList.get(0);
assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(0).getAmount())); assertEquals(Long.valueOf(typeD), first.getTransactionType().getId());
assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), first.getAmount()));
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); assertEquals(parseLocalDate(secondDepositDate, DEFAULT_DATE_FORMAT), first.getDate());
assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(1).getAmount())); GetSavingsAccountTransactionsPageItem second = pageItemsList.get(1);
assertEquals(0, parseDate(firstDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); assertEquals(Long.valueOf(typeD), second.getTransactionType().getId());
assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), second.getAmount()));
assertEquals(parseLocalDate(firstDepositDate, DEFAULT_DATE_FORMAT), second.getDate());
} }
@Test @Test
public void testSavingsTransactionsSearchWithTransactionTypeWithdrawAndDeposit() throws JsonProcessingException { public void testSavingsTransactionsSearchTransactionTypesWithdrawAndDeposit() throws JsonProcessingException {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); int typeD = SavingsAccountTransactionType.DEPOSIT.getId();
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT, Filters.TransactionTypeEnum.WITHDRAWAL)); int typeW = SavingsAccountTransactionType.WITHDRAWAL.getId();
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD) + ',' + typeW);
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE);
searchRequest); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
queryParams);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(3, transactionsResponse.getTotalFilteredRecords()); assertEquals(3, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
assertEquals(3, transactionsResponse.getPageItems().size()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); assertEquals(3, pageItemsList.size());
assertTrue( assertEquals(Long.valueOf(typeW), pageItemsList.get(0).getTransactionType().getId());
Filters.TransactionTypeEnum.WITHDRAWAL.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(0).getAmount()));
assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); assertEquals(Long.valueOf(typeD), pageItemsList.get(1).getTransactionType().getId());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(300), pageItemsList.get(1).getAmount()));
assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); assertEquals(Long.valueOf(typeD), pageItemsList.get(2).getTransactionType().getId());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(2).getTransactionType().getValue())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), pageItemsList.get(2).getAmount()));
assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(2).getAmount()));
} }
@Test @Test
public void testSavingsTransactionsSearchWithPaginationAndNoFilter() { public void testSavingsTransactionsSearchPaginationAndNoFilter() {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters();
int page = 0; TransactionSearchRequest searchParameters = new TransactionSearchRequest().pageable(0, 2, null, null);
int size = 2; Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, null, null);
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, null); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, queryParams);
searchRequest);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(3, transactionsResponse.getTotalFilteredRecords()); assertEquals(3, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
assertEquals(2, transactionsResponse.getPageItems().size()); assertEquals(2, transactionsResponse.getContent().size());
} }
@Test @Test
public void testSavingsTransactionsSearchWithTransactionTypeDepositAndSortByAmountAsc() { public void testSavingsTransactionsSearchTransactionTypeDepositAndSortByAmountAsc() {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "200", withdrawDate, 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)); int typeD = SavingsAccountTransactionType.DEPOSIT.getId();
SortOrder sortOrder = new SortOrder(); TransactionSearchRequest searchParameters = new TransactionSearchRequest().types(String.valueOf(typeD)).pageable(null, null,
sortOrder.setProperty("amount"); "amount", Sort.Direction.ASC);
sortOrder.setDirection(SortOrder.DirectionEnum.ASC); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, DEFAULT_DATE_FORMAT, DEFAULT_LOCALE);
List<SortOrder> sortOrders = List.of(sortOrder); SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchSavingsTransactions(savingsId,
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, sortOrders); queryParams);
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId,
searchRequest);
Assertions.assertNotNull(transactionsResponse); Assertions.assertNotNull(transactionsResponse);
assertEquals(2, transactionsResponse.getTotalFilteredRecords()); assertEquals(2, transactionsResponse.getTotal());
Assertions.assertNotNull(transactionsResponse.getPageItems()); Assertions.assertNotNull(transactionsResponse.getContent());
assertEquals(2, transactionsResponse.getPageItems().size()); List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getContent());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems()); assertEquals(2, pageItemsList.size());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); GetSavingsAccountTransactionsPageItem first = pageItemsList.get(0);
assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); assertEquals(Long.valueOf(typeD), first.getTransactionType().getId());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(100), first.getAmount()));
assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).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 @Test
@@ -270,69 +314,56 @@ public class SavingsAccountTransactionsSearchIntegrationTest {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, 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.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, 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.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.postInterestForSavings(savingsId); this.savingsAccountHelper.postInterestForSavings(savingsId);
Filters filters = new Filters();
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100)));
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500)));
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 06)));
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 01)));
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
SortOrder sortOrder = new SortOrder();
sortOrder.setProperty("amount");
sortOrder.setDirection(SortOrder.DirectionEnum.DESC);
List<SortOrder> sortOrders = List.of(sortOrder);
int page = 0;
int size = 2;
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders);
SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId,
searchRequest);
Assertions.assertNotNull(transactionsResponse);
assertEquals(3, transactionsResponse.getTotalFilteredRecords());
Assertions.assertNotNull(transactionsResponse.getPageItems());
assertEquals(2, transactionsResponse.getPageItems().size());
List<GetSavingsAccountTransactionsPageItem> pageItemsList = List.copyOf(transactionsResponse.getPageItems());
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue()));
assertEquals(0, BigDecimal.valueOf(400).compareTo(pageItemsList.get(0).getAmount()));
assertEquals(0, parseDate(thirdDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate()));
assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue()));
assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount()));
assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate()));
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<String, Object> 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<GetSavingsAccountTransactionsPageItem> 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 @Test
public void testSavingsTransactionsSearchFilterRangeValidationError() { public void testSavingsTransactionsSearchDateValidationError() {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
Assertions.assertNotNull(clientID); Assertions.assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, 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.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID);
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, 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.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); int typeD = SavingsAccountTransactionType.DEPOSIT.getId();
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); TransactionSearchRequest searchParameters = new TransactionSearchRequest().fromAmount(BigDecimal.valueOf(100))
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500))); .toAmount(BigDecimal.valueOf(500));
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(1000))); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, null, null);
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 6))); queryParams.put("fromDate", "05 May 2023"); // wrong date format
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 1))); this.savingsAccountHelperValidationError.searchSavingsTransactions(savingsId, queryParams);
filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 8)));
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT));
SortOrder sortOrder = new SortOrder();
sortOrder.setProperty("amount");
sortOrder.setDirection(SortOrder.DirectionEnum.DESC);
List<SortOrder> sortOrders = List.of(sortOrder);
int page = 0;
int size = 2;
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders);
this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest);
} }
@Test @Test
@@ -346,11 +377,11 @@ public class SavingsAccountTransactionsSearchIntegrationTest {
this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, 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.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID);
Filters filters = new Filters(); TransactionSearchRequest searchParameters = new TransactionSearchRequest();
filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(-100))); Map<String, Object> queryParams = buildTransactionsSearchQuery(searchParameters, null, null);
filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); queryParams.put("fromAmount", "test"); // not number
PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); responseSpecForValidationError.statusCode(404);
this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest); this.savingsAccountHelperValidationError.searchSavingsTransactions(savingsId, queryParams);
} }
private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) {
@@ -372,36 +403,53 @@ public class SavingsAccountTransactionsSearchIntegrationTest {
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
} }
private PagedRequestSavingsTransactionSearch buildTransactionsSearchReqeust(Filters filters, Integer page, Integer size, private Map<String, Object> buildTransactionsSearchQuery(TransactionSearchRequest searchParams, String dateFormat, Locale locale) {
List<SortOrder> sorts) { HashMap<String, Object> params = new HashMap<>();
final Integer DEFAULT_PAGE_SIZE = 50; if (searchParams.getFromDate() != null) {
SavingsTransactionSearch savingsTransactionSearch = new SavingsTransactionSearch(); params.put("fromDate", DateUtils.format(searchParams.getFromDate(), dateFormat, locale));
savingsTransactionSearch.setFilters(filters); }
PagedRequestSavingsTransactionSearch pagedRequest = new PagedRequestSavingsTransactionSearch(); if (searchParams.getToDate() != null) {
pagedRequest.setRequest(savingsTransactionSearch); params.put("toDate", DateUtils.format(searchParams.getToDate(), dateFormat, locale));
pagedRequest.setSorts(sorts != null ? sorts : new ArrayList<>()); }
pagedRequest.setPage(page != null ? page : 0); if (searchParams.getFromSubmittedDate() != null) {
pagedRequest.setSize(size != null ? size : DEFAULT_PAGE_SIZE); params.put("fromSubmittedDate", DateUtils.format(searchParams.getFromSubmittedDate(), dateFormat, locale));
return pagedRequest; }
} if (searchParams.getToSubmittedDate() != null) {
params.put("toSubmittedDate", DateUtils.format(searchParams.getToSubmittedDate(), dateFormat, locale));
private RangeFilterBigDecimal buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum operator, BigDecimal value) { }
RangeFilterBigDecimal transactionAmountFilter = new RangeFilterBigDecimal(); if (searchParams.getFromAmount() != null) {
transactionAmountFilter.setOperator(operator); params.put("fromAmount", searchParams.getFromAmount());
transactionAmountFilter.setValue(value); }
return transactionAmountFilter; if (searchParams.getToAmount() != null) {
} params.put("toAmount", searchParams.getToAmount());
}
private RangeFilterLocalDate buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum operator, LocalDate value) { if (searchParams.getTypes() != null) {
RangeFilterLocalDate transactionDateFilter = new RangeFilterLocalDate(); params.put("types", String.join(",", searchParams.getTypes()));
transactionDateFilter.setOperator(operator); }
transactionDateFilter.setValue(value); if (searchParams.getCredit() != null) {
return transactionDateFilter; params.put("credit", searchParams.getCredit());
} }
if (searchParams.getDebit() != null) {
public static LocalDate parseDate(String dateStr, String pattern) { params.put("debit", searchParams.getDebit());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); }
return LocalDate.parse(dateStr, formatter); 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<Sort.Order> 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 // Reset configuration fields
@@ -18,6 +18,9 @@
*/ */
package org.apache.fineract.integrationtests.common.savings; 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.common.reflect.TypeToken;
import com.google.gson.Gson; import com.google.gson.Gson;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
@@ -36,9 +39,9 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch;
import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
import org.apache.fineract.client.util.JSON; 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.CommonConstants;
import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.Utils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -48,7 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@SuppressWarnings({ "rawtypes" }) @SuppressWarnings({ "rawtypes" })
public class SavingsAccountHelper { public class SavingsAccountHelper extends IntegrationTest {
private final RequestSpecification requestSpec; private final RequestSpecification requestSpec;
private final ResponseSpecification responseSpec; private final ResponseSpecification responseSpec;
@@ -650,12 +653,11 @@ public class SavingsAccountHelper {
return Utils.performServerGet(requestSpec, responseSpec, URL, ""); return Utils.performServerGet(requestSpec, responseSpec, URL, "");
} }
public SavingsAccountTransactionsSearchResponse searchTransactions(Integer savingsId, public SavingsAccountTransactionsSearchResponse searchSavingsTransactions(Integer savingsId, Map<String, Object> queryParams) {
PagedRequestSavingsTransactionSearch searchReqeust) { final String url = SAVINGS_ACCOUNT_URL + "/" + savingsId + "/transactions/search";
final String SAVINGS_TRANSACTIONS_SEARCH_URL = SAVINGS_ACCOUNT_URL + "/" + savingsId + "/transactions/search" + "?" queryParams.put(TENANT_PARAM_NAME, DEFAULT_TENANT);
+ Utils.TENANT_IDENTIFIER; requestSpec.queryParams(queryParams);
String jsonBodyToSend = GSON.toJson(searchReqeust); String response = Utils.performServerGet(this.requestSpec, this.responseSpec, url);
String response = Utils.performServerPost(this.requestSpec, this.responseSpec, SAVINGS_TRANSACTIONS_SEARCH_URL, jsonBodyToSend);
return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class); return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class);
} }