FINERACT-1943 Transaction and datatable GET queries
This commit is contained in:
+7
-4
@@ -30,6 +30,8 @@ import org.apache.fineract.infrastructure.core.service.PaginationHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
|
||||
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
|
||||
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.DatatableCheckStatusData;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.DatatableChecksData;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData;
|
||||
@@ -172,11 +174,12 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat
|
||||
return this.jdbcTemplate.query(sql, this.registerDataTableMapper); // NOSONAR
|
||||
}
|
||||
|
||||
protected static final class RegisterDataTableMapper implements RowMapper<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
|
||||
public DatatableChecksData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
|
||||
final String entity = rs.getString("entity");
|
||||
final String tableName = rs.getString("tableName");
|
||||
|
||||
@@ -184,8 +187,8 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return " t.application_table_name as entity, t.registered_table_name as tableName " + " from x_registered_table t "
|
||||
+ " where application_table_name IN( 'm_client','m_group','m_savings_account','m_loan')";
|
||||
String[] values = EntityTables.getFiltered(EntityTables::hasCheck).stream().map(EntityTables::getName).toArray(String[]::new);
|
||||
return SELECT_FROM + SqlOperator.IN.formatSql(sqlGenerator, JdbcJavaType.VARCHAR, "application_table_name", null, values);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.infrastructure.dataqueries.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -32,8 +33,6 @@ public interface ReadWriteNonCoreDataService {
|
||||
|
||||
DatatableData retrieveDatatable(String datatable);
|
||||
|
||||
List<JsonObject> queryDataTable(String datatable, String columnFilter, String valueFilter, String resultColumns);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')")
|
||||
void registerDatatable(JsonCommand command);
|
||||
|
||||
@@ -73,4 +72,7 @@ public interface ReadWriteNonCoreDataService {
|
||||
String getDataTableName(String Url);
|
||||
|
||||
Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn);
|
||||
|
||||
List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue,
|
||||
@NotNull String resultColumns);
|
||||
}
|
||||
|
||||
+2
-4
@@ -210,8 +210,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
throw new PlatformApiDataValidationException(errors);
|
||||
}
|
||||
|
||||
DatabaseType dialect = databaseTypeResolver.databaseType();
|
||||
Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, dialect);
|
||||
Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, false, sqlGenerator);
|
||||
String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE "
|
||||
+ EQ.formatPlaceholder(sqlGenerator, columnName, null);
|
||||
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue);
|
||||
@@ -1812,7 +1811,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
String queryParamColumnUnderscored;
|
||||
String columnHeaderUnderscored;
|
||||
boolean notFound;
|
||||
DatabaseType dialect = databaseTypeResolver.databaseType();
|
||||
|
||||
final Map<String, Object> affectedColumns = new HashMap<>();
|
||||
final Set<String> keys = queryParams.keySet();
|
||||
@@ -1833,7 +1831,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) {
|
||||
pValue = queryParams.get(key);
|
||||
validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat,
|
||||
clientApplicationLocale, dialect);
|
||||
clientApplicationLocale, true, sqlGenerator);
|
||||
affectedColumns.put(columnHeader.getColumnName(), validatedValue);
|
||||
notFound = false;
|
||||
}
|
||||
|
||||
+32
-10
@@ -37,7 +37,9 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
@@ -49,18 +51,19 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.PagedRequest;
|
||||
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl;
|
||||
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
|
||||
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||
import org.springframework.dao.CannotAcquireLockException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -76,7 +79,7 @@ public class SavingsAccountTransactionsApiResource {
|
||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
|
||||
private final PaymentTypeReadPlatformService paymentTypeReadPlatformService;
|
||||
private final SavingsAccountTransactionsSearchServiceImpl transactionsSearchServiceImpl;
|
||||
private final SavingsAccountTransactionSearchService transactionsSearchService;
|
||||
|
||||
private boolean is(final String commandParam, final String commandValue) {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
@@ -124,16 +127,35 @@ public class SavingsAccountTransactionsApiResource {
|
||||
SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS);
|
||||
}
|
||||
|
||||
@POST
|
||||
@GET
|
||||
@Path("search")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Search Savings Account Transactions")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class))) })
|
||||
public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savingsId") final Long savingsId,
|
||||
@Parameter PagedRequest<SavingsTransactionSearch> searchRequest) {
|
||||
Page<SavingsAccountTransactionData> transactionsData = transactionsSearchServiceImpl.searchTransactions(savingsId, searchRequest);
|
||||
public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savings account id") final Long savingsId,
|
||||
@QueryParam("fromDate") @Parameter(description = "minimum value date (inclusive)", example = "2023-08-08") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "maximum value date (inclusive)", example = "2023-08-15") final String toDate,
|
||||
@QueryParam("fromSubmittedDate") @Parameter(description = "minimum booking date (inclusive)", example = "2023-08-08") final String fromSubmittedDate,
|
||||
@QueryParam("toSubmittedDate") @Parameter(description = "maximum booking date (inclusive)", example = "2023-08-15") final String toSubmittedDate,
|
||||
@QueryParam("fromAmount") @Parameter(description = "minimum transaction amount (inclusive)", example = "1000") final BigDecimal fromAmount,
|
||||
@QueryParam("toAmount") @Parameter(description = "maximum transaction amount (inclusive)", example = "50000000") final BigDecimal toAmount,
|
||||
@QueryParam("types") @Parameter(description = "transaction types", example = "1,2,4,20,21") final String types,
|
||||
@QueryParam("credit") @Parameter(description = "credit") final Boolean credit,
|
||||
@QueryParam("debit") @Parameter(description = "debit") final Boolean debit,
|
||||
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
|
||||
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
|
||||
@QueryParam("orderBy") @Parameter(description = "sort properties", example = "createdDate,transactionDate,id") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sort direction") final Sort.Direction sortOrder,
|
||||
@QueryParam("locale") @Parameter(description = "locale") final String localeString,
|
||||
@QueryParam("dateFormat") @Parameter(description = "date format", example = "yyyy-MM-dd") String dateFormat) {
|
||||
final Locale locale = localeString == null ? null : JsonParserHelper.localeFromString(localeString);
|
||||
TransactionSearchRequest searchParameters = new TransactionSearchRequest().accountId(savingsId)
|
||||
.fromDate(fromDate, dateFormat, locale).toDate(toDate, dateFormat, locale)
|
||||
.fromSubmittedDate(fromSubmittedDate, dateFormat, locale).toSubmittedDate(toSubmittedDate, dateFormat, locale)
|
||||
.fromAmount(fromAmount).toAmount(toAmount).types(types).credit(credit).debit(debit)
|
||||
.pageable(offset, limit, orderBy, sortOrder);
|
||||
Page<SavingsAccountTransactionData> transactionsData = transactionsSearchService.searchTransactions(savingsId, searchParameters);
|
||||
return toApiJsonSerializer.serialize(transactionsData);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -185,8 +185,8 @@ final class SavingsAccountTransactionsApiResourceSwagger {
|
||||
}
|
||||
|
||||
@Schema(example = "2")
|
||||
public Long totalFilteredRecords;
|
||||
public Set<GetSavingsAccountTransactionsPageItem> pageItems;
|
||||
public Long total;
|
||||
public Set<GetSavingsAccountTransactionsPageItem> content;
|
||||
}
|
||||
|
||||
@Schema(description = "PostSavingsAccountTransactionsRequest")
|
||||
|
||||
-95
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-147
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.savings.domain;
|
||||
import jakarta.persistence.LockModeType;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionsSearchRepository;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -29,8 +28,8 @@ import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface SavingsAccountTransactionRepository extends JpaRepository<SavingsAccountTransaction, Long>,
|
||||
JpaSpecificationExecutor<SavingsAccountTransaction>, SavingsTransactionsSearchRepository {
|
||||
public interface SavingsAccountTransactionRepository
|
||||
extends JpaRepository<SavingsAccountTransaction, Long>, JpaSpecificationExecutor<SavingsAccountTransaction> {
|
||||
|
||||
@Query("select sat from SavingsAccountTransaction sat where sat.id = :transactionId and sat.savingsAccount.id = :savingsId")
|
||||
SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId,
|
||||
|
||||
-29
@@ -1,29 +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.util.Map;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BaseQueryParametersMapResult {
|
||||
|
||||
private final String baseQueryString;
|
||||
private final Map<String, Object> parametersMap;
|
||||
}
|
||||
-51
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-39
@@ -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;
|
||||
|
||||
}
|
||||
-27
@@ -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);
|
||||
}
|
||||
-186
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
+42
-40
@@ -1313,52 +1313,54 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
* return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); }
|
||||
*/
|
||||
|
||||
private static final class SavingsAccountTransactionsMapper implements RowMapper<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);
|
||||
sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, ");
|
||||
sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,");
|
||||
sqlBuilder.append(" tr.release_id_of_hold_amount as releaseTransactionId,");
|
||||
sqlBuilder.append(" tr.reason_for_block as reasonForBlock,");
|
||||
sqlBuilder.append("tr.submitted_on_date as submittedOnDate,");
|
||||
sqlBuilder.append(" au.username as submittedByUsername, ");
|
||||
sqlBuilder.append(" nt.note as transactionNote, ");
|
||||
sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,");
|
||||
sqlBuilder.append(
|
||||
"tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, ");
|
||||
sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,");
|
||||
sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,");
|
||||
sqlBuilder.append("fromtran.description as fromTransferDescription,");
|
||||
sqlBuilder.append("totran.id as toTransferId, totran.is_reversed as toTransferReversed,");
|
||||
sqlBuilder.append("totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,");
|
||||
sqlBuilder.append("totran.description as toTransferDescription,");
|
||||
sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,");
|
||||
sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, ");
|
||||
sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, ");
|
||||
sqlBuilder.append(
|
||||
"sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, ");
|
||||
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
||||
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, ");
|
||||
sqlBuilder.append("pt.value as paymentTypeName, ");
|
||||
sqlBuilder.append("tr.is_manual as postInterestAsOn ");
|
||||
sqlBuilder.append("from m_savings_account sa ");
|
||||
sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id ");
|
||||
sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code ");
|
||||
sqlBuilder.append("left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id ");
|
||||
sqlBuilder.append("left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id ");
|
||||
sqlBuilder.append("left join m_payment_detail pd on tr.payment_detail_id = pd.id ");
|
||||
sqlBuilder.append("left join m_payment_type pt on pd.payment_type_id = pt.id ");
|
||||
sqlBuilder.append(" left join m_appuser au on au.id=tr.appuser_id ");
|
||||
sqlBuilder.append(" left join m_note nt ON nt.savings_account_transaction_id=tr.id ");
|
||||
this.schemaSql = sqlBuilder.toString();
|
||||
private static String buildSelect() {
|
||||
return "tr.id as transactionId, tr.transaction_type_enum as transactionType, "
|
||||
+ "tr.transaction_date as transactionDate, tr.amount as transactionAmount, "
|
||||
+ "tr.release_id_of_hold_amount as releaseTransactionId, tr.reason_for_block as reasonForBlock, "
|
||||
+ "tr.submitted_on_date as submittedOnDate, au.username as submittedByUsername, nt.note as transactionNote, "
|
||||
+ "tr.running_balance_derived as runningBalance, tr.is_reversed as reversed, "
|
||||
+ "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, "
|
||||
+ "fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed, "
|
||||
+ "fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount, "
|
||||
+ "fromtran.description as fromTransferDescription, "
|
||||
+ "totran.id as toTransferId, totran.is_reversed as toTransferReversed, "
|
||||
+ "totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, "
|
||||
+ "totran.description as toTransferDescription, sa.id as savingsId, sa.account_no as accountNo, "
|
||||
+ "pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "
|
||||
+ "pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "
|
||||
+ "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "
|
||||
+ "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "
|
||||
+ "curr.display_symbol as currencyDisplaySymbol, pt.value as paymentTypeName, " + "tr.is_manual as postInterestAsOn ";
|
||||
}
|
||||
|
||||
private static String buildFrom() {
|
||||
return " FROM m_savings_account_transaction tr join m_savings_account sa on tr.savings_account_id = sa.id "
|
||||
+ "join m_currency curr on curr.code = sa.currency_code "
|
||||
+ "left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id "
|
||||
+ "left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id "
|
||||
+ "left join m_payment_detail pd on tr.payment_detail_id = pd.id "
|
||||
+ "left join m_payment_type pt on pd.payment_type_id = pt.id left join m_appuser au on au.id=tr.appuser_id "
|
||||
+ "left join m_note nt ON nt.savings_account_transaction_id=tr.id ";
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schemaSql;
|
||||
return SCHEMA;
|
||||
}
|
||||
|
||||
public String select() {
|
||||
return SELECT;
|
||||
}
|
||||
|
||||
public String from() {
|
||||
return FROM;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-5
@@ -18,13 +18,12 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service.search;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.PagedRequest;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
|
||||
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
public interface SavingsAccountTransactionSearchService {
|
||||
|
||||
Page<SavingsAccountTransactionData> searchTransactions(Long savingsId, PagedRequest<SavingsTransactionSearch> searchRequest);
|
||||
|
||||
Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters);
|
||||
}
|
||||
|
||||
+134
-36
@@ -18,21 +18,37 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service.search;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.PagedRequest;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionSearchValidator;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters;
|
||||
import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearchParameters;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.search.data.ColumnFilterData;
|
||||
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -42,35 +58,117 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccountTransactionSearchService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
|
||||
private final SavingsAccountTransactionRepository savingsTransactionRepository;
|
||||
|
||||
private final SavingsAccountTransactionSearchValidator searchValidator;
|
||||
private final GenericDataService genericDataService;
|
||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public Page<SavingsAccountTransactionData> searchTransactions(Long savingsId, PagedRequest<SavingsTransactionSearch> searchRequest) {
|
||||
validateSearchRequest(searchRequest);
|
||||
return executeSearch(savingsId, DepositAccountType.SAVINGS_DEPOSIT, searchRequest);
|
||||
public Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId,
|
||||
@NotNull TransactionSearchRequest searchParameters) {
|
||||
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) {
|
||||
Objects.requireNonNull(searchRequest, "searchRequest must not be null");
|
||||
context.isAuthenticated();
|
||||
Optional<SavingsTransactionSearch> request = searchRequest.getRequest();
|
||||
Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null);
|
||||
searchValidator.validateSearchFilters(searchFilters);
|
||||
private static void addFromToFilter(@NotNull String column, String fromValue, String toValue,
|
||||
@NotNull List<ColumnFilterData> columnFilters) {
|
||||
if (fromValue != null) {
|
||||
columnFilters.add(toValue == null ? ColumnFilterData.create(column, SqlOperator.GTE, fromValue)
|
||||
: ColumnFilterData.btw(column, fromValue, toValue));
|
||||
} else if (toValue != null) {
|
||||
columnFilters.add(ColumnFilterData.create(column, SqlOperator.LTE, toValue));
|
||||
}
|
||||
}
|
||||
|
||||
private Page<SavingsAccountTransactionData> executeSearch(Long savingsId, DepositAccountType depositType,
|
||||
PagedRequest<SavingsTransactionSearch> searchRequest) {
|
||||
Optional<SavingsTransactionSearch> request = searchRequest.getRequest();
|
||||
Pageable pageable = searchRequest.toPageable();
|
||||
Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null);
|
||||
SavingsTransactionSearchParameters searchParameters = SavingsTransactionSearchParameters.builder().savingsId(savingsId)
|
||||
.depositAccountType(DepositAccountType.SAVINGS_DEPOSIT).filters(searchFilters).pageable(pageable).build();
|
||||
org.springframework.data.domain.Page<SavingsAccountTransactionData> pageResult = savingsTransactionRepository
|
||||
.searchTransactions(searchParameters).map(SavingsTransactionSearchResult::toSavingsAccountTransactionData);
|
||||
return new Page<>(pageResult.getContent(), Long.valueOf(pageResult.getTotalElements()).intValue());
|
||||
}
|
||||
@Nullable
|
||||
private static Boolean addTransactionTypesFilter(@NotNull TransactionSearchRequest searchParameters,
|
||||
List<ColumnFilterData> columnFilters) {
|
||||
Predicate<SavingsAccountTransactionType> filter = null;
|
||||
Boolean credit = searchParameters.getCredit();
|
||||
Boolean debit = searchParameters.getDebit();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -23,7 +23,14 @@ import java.util.Set;
|
||||
|
||||
public final class SearchConstants {
|
||||
|
||||
public static final String API_PARAM_RESULTCOLUMNS = "resultColumns";
|
||||
public static final String API_PARAM_COLUMN_FILTERS = "columnFilters";
|
||||
public static final String API_PARAM_COLUMN = "column";
|
||||
public static final String API_PARAM_FILTERS = "filters";
|
||||
public static final String API_PARAM_OPERATOR = "operator";
|
||||
public static final String API_PARAM_VALUES = "values";
|
||||
public static final String API_PARAM_TABLE = "table";
|
||||
public static final String API_PARAM_QUERY = "query";
|
||||
|
||||
private SearchConstants() {}
|
||||
|
||||
|
||||
+51
@@ -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)));
|
||||
}
|
||||
}
|
||||
+54
@@ -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));
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
}
|
||||
+69
-8
@@ -46,10 +46,13 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
|
||||
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
||||
import org.apache.fineract.portfolio.search.data.ColumnFilterData;
|
||||
import org.apache.fineract.portfolio.search.data.FilterData;
|
||||
import org.springframework.jdbc.support.rowset.SqlRowSet;
|
||||
|
||||
public final class SearchUtil {
|
||||
@@ -112,6 +115,7 @@ public final class SearchUtil {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> columnHeaders,
|
||||
List<ApiParameterError> errors, boolean allowEmpty) {
|
||||
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));
|
||||
}
|
||||
if (column != null) {
|
||||
SQLInjectionValidator.validateDynamicQuery(column);
|
||||
if (!columnHeaders.containsKey(column)) {
|
||||
column = camelToSnake(column);
|
||||
if (!columnHeaders.containsKey(column)) {
|
||||
@@ -143,8 +146,64 @@ public final class SearchUtil {
|
||||
return column;
|
||||
}
|
||||
|
||||
public static boolean buildQueryCondition(List<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,
|
||||
Map<String, ResultsetColumnHeaderData> columnHeaders, DatabaseType dialect) {
|
||||
Map<String, ResultsetColumnHeaderData> columnHeaders, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
List<ApiParameterError> errors = new ArrayList<>();
|
||||
column = validateToJdbcColumn(column, columnHeaders, errors, false);
|
||||
if (!errors.isEmpty()) {
|
||||
@@ -152,12 +211,12 @@ public final class SearchUtil {
|
||||
}
|
||||
ResultsetColumnHeaderData columnHeader = columnHeaders.get(column);
|
||||
String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT;
|
||||
return columnHeader.getColumnType().toJdbcValue(dialect,
|
||||
parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, dialect), false);
|
||||
return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(),
|
||||
parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, strict, sqlGenerator), false);
|
||||
}
|
||||
|
||||
public static Object parseAndValidateColumnValue(final ResultsetColumnHeaderData columnHeader, final String pValue,
|
||||
final String dateFormat, final Locale locale, DatabaseType dialect) {
|
||||
final String dateFormat, final Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
String columnValue = pValue;
|
||||
JdbcJavaType colType = columnHeader.getColumnType();
|
||||
if (!colType.isStringType() || !columnHeader.isMandatory()) {
|
||||
@@ -173,7 +232,9 @@ public final class SearchUtil {
|
||||
if (StringUtils.isEmpty(columnValue)) {
|
||||
return columnValue;
|
||||
}
|
||||
SQLInjectionValidator.validateDynamicQuery(columnValue);
|
||||
if (strict) {
|
||||
SQLInjectionValidator.validateDynamicQuery(columnValue);
|
||||
}
|
||||
|
||||
if (columnHeader.hasColumnValues()) {
|
||||
if (columnHeader.isCodeValueDisplayType()) {
|
||||
@@ -193,7 +254,7 @@ public final class SearchUtil {
|
||||
return codeLookup;
|
||||
} else {
|
||||
throw new PlatformDataIntegrityException("error.msg.invalid.columnType.", "Code: " + columnHeader.getColumnName()
|
||||
+ " - Invalid Type " + colType.getJdbcName(dialect) + " (neither varchar nor int)");
|
||||
+ " - Invalid Type " + colType.getJdbcName(sqlGenerator.getDialect()) + " (neither varchar nor int)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -75,7 +74,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
|
||||
@Test
|
||||
public void testSqlInjectionCaughtQueryDataTable() {
|
||||
mockDatatableValidation();
|
||||
assertThrows(SQLInjectionException.class, () -> {
|
||||
assertThrows(PlatformApiDataValidationException.class, () -> {
|
||||
underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1");
|
||||
});
|
||||
}
|
||||
@@ -83,7 +82,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
|
||||
@Test
|
||||
public void testSqlInjectionCaughtQueryDataTable2() {
|
||||
mockDatatableValidation();
|
||||
assertThrows(SQLInjectionException.class, () -> {
|
||||
assertThrows(PlatformApiDataValidationException.class, () -> {
|
||||
underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user