From 5d011077e74833af554c50e218c05f815255c2b5 Mon Sep 17 00:00:00 2001 From: Mkkor Date: Mon, 22 May 2023 20:13:50 +0530 Subject: [PATCH] FINERACT-1912 Refactored Implementation of Savings Account Transaction Search FINERACT-1912 Refactored Implementation of Savings Account Transaction Search FINERACT-1912 Refactored Implementation of Savings Account Transaction Search FINERACT-1912 Savings Account Transaction Advanced Search Support FINERACT-1912 Savings Account Transaction Advanced Search Support Support Pagination, Sorting and Filtering By transaction Type, Transaction Date and Amount Supported Filtering for Amount and Date, GTE, LTE, GT, LT Supported Multiple Transaction Types Filtering --- .../core/data/DataValidatorBuilder.java | 17 + .../core/data/RangeOperator.java | 31 ++ .../domain/AccountTransferTransaction.java | 2 + ...SavingsAccountTransactionsApiResource.java | 19 + ...AccountTransactionsApiResourceSwagger.java | 161 +++++++ ...ingsAccountTransactionSearchValidator.java | 95 ++++ .../data/SavingsTransactionSearchResult.java | 147 +++++++ .../SavingsAccountTransactionRepository.java | 5 +- .../search/BaseQueryParametersMapResult.java | 29 ++ .../search/SavingsTransactionSearch.java | 51 +++ .../SavingsTransactionSearchParameters.java | 39 ++ .../SavingsTransactionsSearchRepository.java | 27 ++ ...vingsTransactionsSearchRepositoryImpl.java | 186 ++++++++ ...avingsAccountTransactionSearchService.java | 30 ++ ...sAccountTransactionsSearchServiceImpl.java | 76 ++++ ...ountTransactionsSearchIntegrationTest.java | 414 ++++++++++++++++++ .../common/savings/SavingsAccountHelper.java | 13 + 17 files changed, 1340 insertions(+), 2 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java index 39b367c73..b1313d24c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java @@ -275,6 +275,23 @@ public class DataValidatorBuilder { return this; } + public DataValidatorBuilder notExceedingListLengthOf(final Integer maxLength) { + if (this.value == null && this.ignoreNullValue) { + return this; + } + + if (this.value instanceof List && ((List) this.value).size() > maxLength) { + final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(this.resource).append(".") + .append(this.parameter).append(".exceeds.max.length.allowed"); + final StringBuilder defaultEnglishMessage = new StringBuilder("The parameter `").append(this.parameter) + .append("` exceeds allowed max length of ").append(maxLength).append("."); + final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), + defaultEnglishMessage.toString(), this.parameter); + this.dataValidationErrors.add(error); + } + return this; + } + public DataValidatorBuilder inMinMaxRange(final Integer min, final Integer max) { if (this.value == null && this.ignoreNullValue) { return this; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java new file mode 100644 index 000000000..67977e4b2 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/RangeOperator.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum RangeOperator { + + GTE(">="), LTE("<="), GT(">"), LT("<"); + + private final String symbol; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java index 7857dd47f..cb6608741 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java @@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDate; +import lombok.Getter; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -34,6 +35,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; @Entity @Table(name = "m_account_transfer_transaction") +@Getter public class AccountTransferTransaction extends AbstractPersistableCustom { @ManyToOne diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index 4ca7650c3..bbbc66218 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.api; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -48,13 +49,17 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.PagedRequest; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; +import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionsSearchServiceImpl; import org.springframework.dao.CannotAcquireLockException; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; @@ -71,6 +76,7 @@ public class SavingsAccountTransactionsApiResource { private final ApiRequestParameterHelper apiRequestParameterHelper; private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; private final PaymentTypeReadPlatformService paymentTypeReadPlatformService; + private final SavingsAccountTransactionsSearchServiceImpl transactionsSearchServiceImpl; private boolean is(final String commandParam, final String commandValue) { return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); @@ -118,6 +124,19 @@ public class SavingsAccountTransactionsApiResource { SavingsApiSetConstants.SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS); } + @POST + @Path("search") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Search Savings Account Transactions") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.SavingsAccountTransactionsSearchResponse.class))) }) + public String searchTransactions(@PathParam("savingsId") @Parameter(description = "savingsId") final Long savingsId, + @Parameter PagedRequest searchRequest) { + Page transactionsData = transactionsSearchServiceImpl.searchTransactions(savingsId, searchRequest); + return toApiJsonSerializer.serialize(transactionsData); + } + @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java index 3bd433b16..d1c7e7311 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResourceSwagger.java @@ -20,11 +20,172 @@ package org.apache.fineract.portfolio.savings.api; import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; final class SavingsAccountTransactionsApiResourceSwagger { private SavingsAccountTransactionsApiResourceSwagger() {} + @Schema(description = "SavingsAccountTransactionsSearchResponse") + public static final class SavingsAccountTransactionsSearchResponse { + + private SavingsAccountTransactionsSearchResponse() {} + + static final class GetSavingsAccountTransactionsPageItem { + + private GetSavingsAccountTransactionsPageItem() {} + + static final class GetTranscationEnumData { + + private GetTranscationEnumData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "savingsAccountTransactionType.deposit") + public String code; + @Schema(example = "Deposit") + public String value; + + @Schema(example = "true") + public boolean deposit; + @Schema(example = "false") + public boolean dividendPayout; + @Schema(example = "false") + public boolean withdrawal; + @Schema(example = "false") + public boolean interestPosting; + @Schema(example = "false") + public boolean feeDeduction; + @Schema(example = "false") + public boolean initiateTransfer; + @Schema(example = "false") + public boolean approveTransfer; + @Schema(example = "false") + public boolean withdrawTransfer; + @Schema(example = "false") + public boolean rejectTransfer; + @Schema(example = "false") + public boolean overdraftInterest; + @Schema(example = "false") + public boolean writtenoff; + @Schema(example = "true") + public boolean overdraftFee; + @Schema(example = "false") + public boolean withholdTax; + @Schema(example = "false") + public boolean escheat; + @Schema(example = "false") + public boolean amountHold; + @Schema(example = "false") + public boolean amountRelease; + } + + static final class GetTransactionsCurrency { + + private GetTransactionsCurrency() {} + + @Schema(example = "USD") + public String code; + @Schema(example = "US Dollar") + public String name; + @Schema(example = "2") + public Integer decimalPlaces; + @Schema(example = "0") + public Integer isMultiplesOf; + @Schema(example = "$") + public String displaySymbol; + @Schema(example = "currency.USD") + public String nameCode; + @Schema(example = "US Dollar ($)") + public String displayLabel; + } + + static final class GetTransactionsPaymentDetailData { + + private GetTransactionsPaymentDetailData() {} + + static final class GetPaymentTypeData { + + private GetPaymentTypeData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "Money Transfer") + public String name; + @Schema(example = "false") + public Boolean isSystemDefined; + + } + + @Schema(example = "1") + public Long id; + public GetPaymentTypeData paymentType; + @Schema(example = "acc123") + public String accountNumber; + @Schema(example = "che123") + public String checkNumber; + @Schema(example = "rou123") + public String routingCode; + @Schema(example = "rec123") + public String receiptNumber; + @Schema(example = "ban123") + public String bankNumber; + } + + static final class GetSavingsAccountChargesPaidByData { + + private GetSavingsAccountChargesPaidByData() {} + + @Schema(example = "1") + public Long chargeId; + @Schema(example = "0") + public BigDecimal amount; + } + + @Schema(example = "1") + public Long id; + public GetTranscationEnumData transactionType; + @Schema(example = "1") + public Long accountId; + @Schema(example = "000000001") + public String accountNo; + @Schema(example = "[2023, 05, 01]") + public LocalDate date; + public GetTransactionsCurrency currency; + public GetTransactionsPaymentDetailData paymentDetailData; + @Schema(example = "500") + public BigDecimal amount; + @Schema(example = "500") + public BigDecimal runningBalance; + @Schema(example = "false") + public boolean reversed; + @Schema(example = "[2023, 05, 01]") + public LocalDate submittedOnDate; + @Schema(example = "false") + public boolean interestedPostedAsOn; + @Schema(example = "mifos") + public String submittedByUsername; + @Schema(example = "false") + public boolean isManualTransaction; + @Schema(example = "false") + public Boolean isReversal; + @Schema(example = "0") + public Long originalTransactionId; + @Schema(example = "false") + public Boolean lienTransaction; + @Schema(example = "0") + public Long releaseTransactionId; + public Set chargesPaidByData = new HashSet<>(); + + } + + @Schema(example = "2") + public Long totalFilteredRecords; + public Set pageItems; + } + @Schema(description = "PostSavingsAccountTransactionsRequest") public static final class PostSavingsAccountTransactionsRequest { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java new file mode 100644 index 000000000..39a17334a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionSearchValidator.java @@ -0,0 +1,95 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.data; + +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.data.RangeOperator; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter; +import org.springframework.stereotype.Component; + +@Component +public class SavingsAccountTransactionSearchValidator { + + public void validateSearchFilters(SavingsTransactionSearch.Filters searchFilters) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SavingsApiConstants.SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME); + if (searchFilters != null) { + + List> dateFilters = searchFilters.getTransactionDate(); + validateRangeFilters(baseDataValidator, dateFilters, transactionDateParamName); + + List> amountFilters = searchFilters.getTransactionAmount(); + validateRangeFilters(baseDataValidator, amountFilters, transactionAmountParamName); + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void validateRangeFilters(DataValidatorBuilder baseDataValidator, List> rangeFilters, String paramName) { + if (rangeFilters == null) { + return; + } + + if (rangeFilters.size() > 2) { + baseDataValidator.parameter(paramName).value(rangeFilters).notExceedingListLengthOf(2); + } + + if (!rangeFilters.isEmpty()) { + RangeFilter firstFilter = rangeFilters.get(0); + RangeOperator firstOperator = firstFilter.getOperator(); + + if (rangeFilters.size() == 2) { + RangeFilter secondFilter = rangeFilters.get(1); + RangeOperator secondOperator = secondFilter.getOperator(); + + if (((firstOperator == RangeOperator.GT || firstOperator == RangeOperator.GTE) + && !(secondOperator == RangeOperator.LT || secondOperator == RangeOperator.LTE)) + || ((firstOperator == RangeOperator.LT || firstOperator == RangeOperator.LTE) + && !(secondOperator == RangeOperator.GT || secondOperator == RangeOperator.GTE))) { + baseDataValidator.parameter(paramName).failWithCode("invalid.range", firstOperator, secondOperator); + } + } + } + + for (RangeFilter filter : rangeFilters) { + T value = filter.getValue(); + if (value instanceof BigDecimal) { + baseDataValidator.parameter(paramName).value(value).zeroOrPositiveAmount(); + } + } + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java new file mode 100644 index 000000000..4ab2ad47c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsTransactionSearchResult.java @@ -0,0 +1,147 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction; +import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.apache.fineract.useradministration.domain.AppUser; + +@Getter +@AllArgsConstructor +public class SavingsTransactionSearchResult { + + private Long transactionId; + private Integer transactionType; + private LocalDate transactionDate; + private BigDecimal transactionAmount; + private Long releaseIdOfHoldAmountTransaction; + private String reasonForBlock; + private LocalDateTime createdDate; + private AppUser appUser; + private String note; + private BigDecimal runningBalance; + private boolean reversed; + private boolean reversalTransaction; + private Long originalTxnId; + private Boolean lienTransaction; + private boolean isManualTransaction; + private AccountTransferTransaction fromSavingsTransaction; + private AccountTransferTransaction toSavingsTransaction; + private SavingsAccount savingsAccount; + private PaymentDetail paymentDetail; + private ApplicationCurrency currency; + + public static final SavingsAccountTransactionData toSavingsAccountTransactionData(SavingsTransactionSearchResult dto) { + final Long id = dto.getTransactionId(); + final int transactionTypeInt = dto.getTransactionType(); + + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); + + final LocalDate date = dto.getTransactionDate(); + final LocalDate submittedOnDate = Optional.ofNullable(dto.getCreatedDate()).map(LocalDateTime::toLocalDate).orElse(null); + final BigDecimal amount = Optional.ofNullable(dto.getTransactionAmount()).orElse(BigDecimal.ZERO); + final Long releaseTransactionId = Optional.ofNullable(dto.getReleaseIdOfHoldAmountTransaction()).orElse(null); + final String reasonForBlock = Optional.ofNullable(dto.getReasonForBlock()).orElse(null); + final BigDecimal outstandingChargeAmount = null; + final BigDecimal runningBalance = Optional.ofNullable(dto.getRunningBalance()).orElse(BigDecimal.ZERO); + final boolean reversed = dto.isReversed(); + final boolean isReversal = dto.isReversalTransaction(); + final Long originalTransactionId = Optional.ofNullable(dto.getOriginalTxnId()).orElse(null); + final Boolean lienTransaction = dto.getLienTransaction(); + + final Long savingsId = Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount -> savingsAccount.getId()).orElse(null); + final String accountNo = Optional.ofNullable(dto.getSavingsAccount()).map(savingsAccount -> savingsAccount.getAccountNumber()) + .orElse(null); + final boolean postInterestAsOn = dto.isManualTransaction(); + + PaymentDetailData paymentDetailData = null; + if (Objects.nonNull(transactionType) && transactionType.isDepositOrWithdrawal()) { + final PaymentDetail paymentDetail = dto.getPaymentDetail(); + if (Objects.nonNull(paymentDetail)) { + final Long paymentTypeId = Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType -> paymentType.getId()) + .orElse(null); + if (Objects.nonNull(paymentTypeId)) { + final String typeName = Optional.ofNullable(paymentDetail.getPaymentType()).map(paymentType -> paymentType.getName()) + .orElse(null); + final PaymentTypeData paymentType = PaymentTypeData.instance(paymentTypeId, typeName); + final String accountNumber = paymentDetail.getAccountNumber(); + final String checkNumber = paymentDetail.getCheckNumber(); + final String routingCode = paymentDetail.getRoutingCode(); + final String receiptNumber = paymentDetail.getReceiptNumber(); + final String bankNumber = paymentDetail.getBankNumber(); + paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, + bankNumber); + } + } + } + + final String currencyCode = dto.getSavingsAccount().getCurrency().getCode(); + final String currencyName = dto.getCurrency().getName(); + final String currencyNameCode = dto.getCurrency().getNameCode(); + final String currencyDisplaySymbol = dto.getCurrency().getDisplaySymbol(); + final Integer currencyDigits = dto.getSavingsAccount().getCurrency().getDigitsAfterDecimal(); + final Integer inMultiplesOf = dto.getSavingsAccount().getCurrency().getCurrencyInMultiplesOf(); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + AccountTransferData transfer = null; + AccountTransferTransaction transferFrom = dto.getFromSavingsTransaction(); + AccountTransferTransaction transferTo = dto.getToSavingsTransaction(); + if (Objects.nonNull(transferFrom)) { + final Long fromTransferId = transferFrom.getId(); + final LocalDate fromTransferDate = transferFrom.getDate(); + final BigDecimal fromTransferAmount = Optional.ofNullable(transferFrom.getAmount()).orElse(BigDecimal.ZERO); + final boolean fromTransferReversed = transferFrom.isReversed(); + final String fromTransferDescription = transferFrom.getDescription(); + + transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, + fromTransferDescription, fromTransferReversed); + } else if (Objects.nonNull(transferTo)) { + final Long toTransferId = transferTo.getId(); + final LocalDate toTransferDate = transferTo.getDate(); + final BigDecimal toTransferAmount = Optional.ofNullable(transferTo.getAmount()).orElse(BigDecimal.ZERO); + final boolean toTransferReversed = transferTo.isReversed(); + final String toTransferDescription = transferTo.getDescription(); + + transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, + toTransferDescription, toTransferReversed); + } + final String submittedByUsername = Optional.ofNullable(dto.getAppUser()).map(user -> user.getUsername()).orElse(null); + final String note = Optional.ofNullable(dto.getNote()).orElse(null); + return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, amount, + outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, submittedByUsername, note, + isReversal, originalTransactionId, lienTransaction, releaseTransactionId, reasonForBlock); + + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java index 2eba64bb2..ae0709658 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.domain; import jakarta.persistence.LockModeType; import java.time.LocalDate; import java.util.List; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionsSearchRepository; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -28,8 +29,8 @@ import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface SavingsAccountTransactionRepository - extends JpaRepository, JpaSpecificationExecutor { +public interface SavingsAccountTransactionRepository extends JpaRepository, + JpaSpecificationExecutor, SavingsTransactionsSearchRepository { @Query("select sat from SavingsAccountTransaction sat where sat.id = :transactionId and sat.savingsAccount.id = :savingsId") SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java new file mode 100644 index 000000000..4522f0169 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/BaseQueryParametersMapResult.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain.search; + +import java.util.Map; +import lombok.Data; + +@Data +public class BaseQueryParametersMapResult { + + private final String baseQueryString; + private final Map parametersMap; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java new file mode 100644 index 000000000..ef09050bb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearch.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain.search; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.Data; +import org.apache.fineract.infrastructure.core.data.RangeOperator; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; + +@Data +public class SavingsTransactionSearch { + + private Filters filters; + + @Data + public static class Filters { + + private List> transactionDate; + + private List> transactionAmount; + + private List transactionType; + } + + @Data + public static class RangeFilter { + + private RangeOperator operator; + + private T value; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java new file mode 100644 index 000000000..4ec2f4dd8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionSearchParameters.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain.search; + +import lombok.Builder; +import lombok.Getter; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; +import org.springframework.data.domain.Pageable; + +@Getter +@Builder +public class SavingsTransactionSearchParameters { + + private Long savingsId; + + private DepositAccountType depositAccountType; + + private Filters filters; + + private Pageable pageable; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java new file mode 100644 index 000000000..8a881154e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepository.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain.search; + +import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; +import org.springframework.data.domain.Page; + +public interface SavingsTransactionsSearchRepository { + + Page searchTransactions(SavingsTransactionSearchParameters searchParameters); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java new file mode 100644 index 000000000..2a92fea05 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/search/SavingsTransactionsSearchRepositoryImpl.java @@ -0,0 +1,186 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain.search; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.RangeFilter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class SavingsTransactionsSearchRepositoryImpl implements SavingsTransactionsSearchRepository { + + private static final String AMOUNT_FIELD_NAME = "amount"; + private static final String ID_FIELD_NAME = "id"; + private static final String CREATED_DATE_FIELD_NAME = "createdDate"; + private static final String TRANSACTION_DATE_FIELD_NAME = "dateOf"; + private final EntityManager entityManager; + + @Override + public Page searchTransactions(SavingsTransactionSearchParameters searchParameters) { + // Build base query with filters but without the selection + BaseQueryParametersMapResult baseQueryParameterMapResult = buildBaseQueryWithFilters(searchParameters.getSavingsId(), + searchParameters.getDepositAccountType(), searchParameters.getFilters()); + + // Attach the selection + String jpqlQuery = attachSelection(baseQueryParameterMapResult.getBaseQueryString()); + + // Attach the ordering + String queryWithOrdering = attachOrdering(jpqlQuery, searchParameters.getPageable().getSort()); + + // Execute Query + TypedQuery queryToExecute = entityManager.createQuery(queryWithOrdering, + SavingsTransactionSearchResult.class); + setQueryParameters(queryToExecute, baseQueryParameterMapResult.getParametersMap()); + applyPagination(queryToExecute, searchParameters.getPageable()); + List resultList = queryToExecute.getResultList(); + + // Attach the count selection + String countQuery = attachCountSelection(baseQueryParameterMapResult.getBaseQueryString()); + + // Execute count query + TypedQuery countQueryToExecute = entityManager.createQuery(countQuery, Long.class); + setQueryParameters(countQueryToExecute, baseQueryParameterMapResult.getParametersMap()); + Long totalElements = countQueryToExecute.getSingleResult(); + + return PageableExecutionUtils.getPage(resultList, searchParameters.getPageable(), () -> totalElements); + } + + private void setQueryParameters(TypedQuery queryToExecute, Map parametersMap) { + for (Map.Entry entry : parametersMap.entrySet()) { + queryToExecute.setParameter(entry.getKey(), entry.getValue()); + } + } + + private BaseQueryParametersMapResult buildBaseQueryWithFilters(Long savingsId, DepositAccountType depositAccountType, Filters filters) { + String baseQuery = """ + SELECT tr + FROM SavingsAccountTransaction tr + JOIN ApplicationCurrency currency ON (currency.code = tr.savingsAccount.currency.code) + LEFT JOIN AccountTransferTransaction fromtran ON (fromtran.fromSavingsTransaction = tr) + LEFT JOIN AccountTransferTransaction totran ON (totran.toSavingsTransaction = tr) + LEFT JOIN tr.notes nt ON (nt.savingsTransaction = tr) + WHERE tr.savingsAccount.id = :savingsId + AND tr.savingsAccount.depositType = :depositType + """; + StringBuilder baseQueryBuilder = new StringBuilder(baseQuery); + + Map parameterMap = new HashMap<>(); + parameterMap.put("savingsId", savingsId); + parameterMap.put("depositType", depositAccountType.getValue()); + + setFilterConditions(baseQueryBuilder, parameterMap, filters); + return new BaseQueryParametersMapResult(baseQueryBuilder.toString(), parameterMap); + } + + private String attachSelection(String baseQuery) { + return baseQuery.replace("SELECT tr", + "SELECT NEW org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult(tr.id,tr.typeOf, tr.dateOf, tr.amount, tr.releaseIdOfHoldAmountTransaction, tr.reasonForBlock,tr.createdDate, tr.appUser, nt.note, tr.runningBalance, tr.reversed,tr.reversalTransaction, tr.originalTxnId, tr.lienTransaction, tr.isManualTransaction,fromTran, toTran, tr.savingsAccount, tr.paymentDetail, currency) "); + } + + private String attachOrdering(String jpqlQuery, Sort sort) { + StringJoiner orderByClauseBuilder = new StringJoiner(", ", " ORDER BY ", ""); + + if (Objects.nonNull(sort) && sort.isSorted()) { + buildOrderByClause(sort.toList(), orderByClauseBuilder); + } else { + List defaultOrders = getDefaultOrders(); + buildOrderByClause(defaultOrders, orderByClauseBuilder); + } + return new StringBuilder(jpqlQuery).append(orderByClauseBuilder.toString()).toString(); + } + + private void buildOrderByClause(List orders, StringJoiner orderByClauseBuilder) { + for (Order order : orders) { + String property = "tr." + order.getProperty(); + String direction = order.getDirection().name(); + String orderByExpression = new StringBuilder(property).append(StringUtils.SPACE).append(direction).toString(); + orderByClauseBuilder.add(orderByExpression); + } + } + + private List getDefaultOrders() { + return List.of(Order.desc(TRANSACTION_DATE_FIELD_NAME), Order.desc(CREATED_DATE_FIELD_NAME), Order.desc(ID_FIELD_NAME)); + } + + private void applyPagination(TypedQuery query, Pageable pageable) { + if (pageable.isPaged()) { + query.setFirstResult((int) pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + } + } + + private String attachCountSelection(String baseQuery) { + return baseQuery.replace("SELECT tr", "SELECT COUNT(tr) "); + } + + private void setFilterConditions(StringBuilder queryBuilder, Map parameterMap, Filters filters) { + if (Objects.nonNull(filters)) { + List> dateFilters = filters.getTransactionDate(); + List> amountFilters = filters.getTransactionAmount(); + List transactionTypes = filters.getTransactionType(); + + if (Objects.nonNull(dateFilters)) { + processRangeFilters(queryBuilder, parameterMap, dateFilters, TRANSACTION_DATE_FIELD_NAME); + } + + if (Objects.nonNull(amountFilters)) { + processRangeFilters(queryBuilder, parameterMap, amountFilters, AMOUNT_FIELD_NAME); + } + + if (CollectionUtils.isNotEmpty(transactionTypes)) { + List transactionTypeValues = transactionTypes.stream().map(SavingsAccountTransactionType::getValue) + .collect(Collectors.toList()); + queryBuilder.append(" AND tr.typeOf IN :transactionTypes "); + parameterMap.put("transactionTypes", transactionTypeValues); + } + } + } + + private void processRangeFilters(StringBuilder queryBuilder, Map parameterMap, List> filters, + String field) { + filters.forEach(filter -> { + String paramName = new StringBuilder(field).append(filter.getOperator()).toString(); + queryBuilder.append(" AND tr.").append(field).append(StringUtils.SPACE).append(filter.getOperator().getSymbol()).append(" :") + .append(paramName); + parameterMap.put(paramName, filter.getValue()); + }); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java new file mode 100644 index 000000000..e330aeaed --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service.search; + +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; + +public interface SavingsAccountTransactionSearchService { + + Page searchTransactions(Long savingsId, PagedRequest searchRequest); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java new file mode 100644 index 000000000..0d477e00c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java @@ -0,0 +1,76 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service.search; + +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionSearchValidator; +import org.apache.fineract.portfolio.savings.data.SavingsTransactionSearchResult; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearch.Filters; +import org.apache.fineract.portfolio.savings.domain.search.SavingsTransactionSearchParameters; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccountTransactionSearchService { + + private final PlatformSecurityContext context; + + private final SavingsAccountTransactionRepository savingsTransactionRepository; + + private final SavingsAccountTransactionSearchValidator searchValidator; + + @Override + public Page searchTransactions(Long savingsId, PagedRequest searchRequest) { + validateSearchRequest(searchRequest); + return executeSearch(savingsId, DepositAccountType.SAVINGS_DEPOSIT, searchRequest); + } + + private void validateSearchRequest(PagedRequest searchRequest) { + Objects.requireNonNull(searchRequest, "searchRequest must not be null"); + context.isAuthenticated(); + Optional request = searchRequest.getRequest(); + Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); + searchValidator.validateSearchFilters(searchFilters); + } + + private Page executeSearch(Long savingsId, DepositAccountType depositType, + PagedRequest searchRequest) { + Optional request = searchRequest.getRequest(); + Pageable pageable = searchRequest.toPageable(); + Filters searchFilters = request.map(SavingsTransactionSearch::getFilters).orElse(null); + SavingsTransactionSearchParameters searchParameters = SavingsTransactionSearchParameters.builder().savingsId(savingsId) + .depositAccountType(DepositAccountType.SAVINGS_DEPOSIT).filters(searchFilters).pageable(pageable).build(); + org.springframework.data.domain.Page pageResult = savingsTransactionRepository + .searchTransactions(searchParameters).map(SavingsTransactionSearchResult::toSavingsAccountTransactionData); + return new Page<>(pageResult.getContent(), Long.valueOf(pageResult.getTotalElements()).intValue()); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java new file mode 100644 index 000000000..c89440375 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionsSearchIntegrationTest.java @@ -0,0 +1,414 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.fineract.client.models.Filters; +import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem; +import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch; +import org.apache.fineract.client.models.RangeFilterBigDecimal; +import org.apache.fineract.client.models.RangeFilterLocalDate; +import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; +import org.apache.fineract.client.models.SavingsTransactionSearch; +import org.apache.fineract.client.models.SortOrder; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({ "rawtypes" }) +public class SavingsAccountTransactionsSearchIntegrationTest { + + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + public static final String DEFAULT_DATE_FORMAT = "dd MMM yyyy"; + final String startDate = "01 May 2023"; + final String firstDepositDate = "05 May 2023"; + final String secondDepositDate = "09 May 2023"; + final String thirdDepositDate = "12 May 2023"; + final String fourthDepositDate = "01 Jun 2023"; + final String withdrawDate = "10 May 2023"; + + private ResponseSpecification responseSpec; + private ResponseSpecification responseSpecForValidationError; + private RequestSpecification requestSpec; + private SavingsProductHelper savingsProductHelper; + private SavingsAccountHelper savingsAccountHelper; + private SavingsAccountHelper savingsAccountHelperValidationError; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.responseSpecForValidationError = new ResponseSpecBuilder().expectStatusCode(400).build(); + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + this.savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, this.responseSpecForValidationError); + this.savingsProductHelper = new SavingsProductHelper(); + } + + @Test + public void testSavingsTransactionsSearchWithAmountFilterLteGte() throws JsonProcessingException { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LTE, BigDecimal.valueOf(200))); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(1, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + BigDecimal expectedAmount = BigDecimal.valueOf(100); + assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); + } + + @Test + public void testSavingsTransactionsSearchWithAmountFilterLtGt() throws JsonProcessingException { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", startDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GT, BigDecimal.valueOf(100))); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(400))); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(1, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + BigDecimal expectedAmount = BigDecimal.valueOf(300); + assertEquals(0, expectedAmount.compareTo(pageItemsList.get(0).getAmount())); + } + + @Test + public void testSavingsTransactionsSearchWithDateFilterLteGte() throws JsonProcessingException { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GTE, LocalDate.of(2023, 05, 06))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 05, 10))); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(2, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(2, transactionsResponse.getPageItems().size()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + assertEquals(0, parseDate(withdrawDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); + assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + } + + @Test + public void testSavingsTransactionsSearchWithTransactionTypeDepositAndDefaultSort() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(2, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(2, transactionsResponse.getPageItems().size()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(0).getAmount())); + assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(1).getAmount())); + assertEquals(0, parseDate(firstDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + } + + @Test + public void testSavingsTransactionsSearchWithTransactionTypeWithdrawAndDeposit() throws JsonProcessingException { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT, Filters.TransactionTypeEnum.WITHDRAWAL)); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(3, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(3, transactionsResponse.getPageItems().size()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + assertTrue( + Filters.TransactionTypeEnum.WITHDRAWAL.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(2).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(2).getAmount())); + } + + @Test + public void testSavingsTransactionsSearchWithPaginationAndNoFilter() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + Filters filters = new Filters(); + int page = 0; + int size = 2; + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, null); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(3, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(2, transactionsResponse.getPageItems().size()); + } + + @Test + public void testSavingsTransactionsSearchWithTransactionTypeDepositAndSortByAmountAsc() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "200", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + Filters filters = new Filters(); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); + SortOrder sortOrder = new SortOrder(); + sortOrder.setProperty("amount"); + sortOrder.setDirection(SortOrder.DirectionEnum.ASC); + List sortOrders = List.of(sortOrder); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, sortOrders); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(2, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(2, transactionsResponse.getPageItems().size()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(100).compareTo(pageItemsList.get(0).getAmount())); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); + } + + @Test + public void testSavingsTransactionsSearchWithFiltersSortingAndPagination() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + + this.savingsAccountHelper.postInterestForSavings(savingsId); + Filters filters = new Filters(); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 06))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 01))); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); + SortOrder sortOrder = new SortOrder(); + sortOrder.setProperty("amount"); + sortOrder.setDirection(SortOrder.DirectionEnum.DESC); + List sortOrders = List.of(sortOrder); + int page = 0; + int size = 2; + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders); + SavingsAccountTransactionsSearchResponse transactionsResponse = this.savingsAccountHelper.searchTransactions(savingsId, + searchRequest); + Assertions.assertNotNull(transactionsResponse); + assertEquals(3, transactionsResponse.getTotalFilteredRecords()); + Assertions.assertNotNull(transactionsResponse.getPageItems()); + assertEquals(2, transactionsResponse.getPageItems().size()); + List pageItemsList = List.copyOf(transactionsResponse.getPageItems()); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(0).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(400).compareTo(pageItemsList.get(0).getAmount())); + assertEquals(0, parseDate(thirdDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(0).getDate())); + assertTrue(Filters.TransactionTypeEnum.DEPOSIT.getValue().equalsIgnoreCase(pageItemsList.get(1).getTransactionType().getValue())); + assertEquals(0, BigDecimal.valueOf(300).compareTo(pageItemsList.get(1).getAmount())); + assertEquals(0, parseDate(secondDepositDate, DEFAULT_DATE_FORMAT).compareTo(pageItemsList.get(1).getDate())); + + } + + @Test + public void testSavingsTransactionsSearchFilterRangeValidationError() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(100))); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(500))); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.LT, BigDecimal.valueOf(1000))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.GT, LocalDate.of(2023, 05, 6))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 1))); + filters.addTransactionDateItem(buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum.LTE, LocalDate.of(2023, 06, 8))); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); + SortOrder sortOrder = new SortOrder(); + sortOrder.setProperty("amount"); + sortOrder.setDirection(SortOrder.DirectionEnum.DESC); + List sortOrders = List.of(sortOrder); + int page = 0; + int size = 2; + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, page, size, sortOrders); + this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest); + } + + @Test + public void testSavingsTransactionsSearchTransactionAmountValidationError() { + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate); + Assertions.assertNotNull(clientID); + final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDate); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", firstDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "300", secondDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "50", withdrawDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "400", thirdDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "200", fourthDepositDate, CommonConstants.RESPONSE_RESOURCE_ID); + + Filters filters = new Filters(); + filters.addTransactionAmountItem(buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum.GTE, BigDecimal.valueOf(-100))); + filters.setTransactionType(List.of(Filters.TransactionTypeEnum.DEPOSIT)); + PagedRequestSavingsTransactionSearch searchRequest = buildTransactionsSearchReqeust(filters, null, null, null); + this.savingsAccountHelperValidationError.searchTransactions(savingsId, searchRequest); + } + + private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { + final Integer savingsProductID = createSavingsProductDailyPosting(); + Assertions.assertNotNull(savingsProductID); + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, + ACCOUNT_TYPE_INDIVIDUAL, startDate); + Assertions.assertNotNull(savingsId); + HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + return savingsId; + } + + private Integer createSavingsProductDailyPosting() { + final String savingsProductJSON = this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + private PagedRequestSavingsTransactionSearch buildTransactionsSearchReqeust(Filters filters, Integer page, Integer size, + List sorts) { + final Integer DEFAULT_PAGE_SIZE = 50; + SavingsTransactionSearch savingsTransactionSearch = new SavingsTransactionSearch(); + savingsTransactionSearch.setFilters(filters); + PagedRequestSavingsTransactionSearch pagedRequest = new PagedRequestSavingsTransactionSearch(); + pagedRequest.setRequest(savingsTransactionSearch); + pagedRequest.setSorts(sorts != null ? sorts : new ArrayList<>()); + pagedRequest.setPage(page != null ? page : 0); + pagedRequest.setSize(size != null ? size : DEFAULT_PAGE_SIZE); + return pagedRequest; + } + + private RangeFilterBigDecimal buildTransactionAmountRange(RangeFilterBigDecimal.OperatorEnum operator, BigDecimal value) { + RangeFilterBigDecimal transactionAmountFilter = new RangeFilterBigDecimal(); + transactionAmountFilter.setOperator(operator); + transactionAmountFilter.setValue(value); + return transactionAmountFilter; + } + + private RangeFilterLocalDate buildTransactionDateRange(RangeFilterLocalDate.OperatorEnum operator, LocalDate value) { + RangeFilterLocalDate transactionDateFilter = new RangeFilterLocalDate(); + transactionDateFilter.setOperator(operator); + transactionDateFilter.setValue(value); + return transactionDateFilter; + } + + public static LocalDate parseDate(String dateStr, String pattern) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return LocalDate.parse(dateStr, formatter); + } + + // Reset configuration fields + @AfterEach + public void tearDown() { + GlobalConfigurationHelper.resetAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec); + GlobalConfigurationHelper.verifyAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index 1c8f66585..7f6cc2d0f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -36,6 +36,9 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import org.apache.fineract.client.models.PagedRequestSavingsTransactionSearch; +import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; +import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -49,6 +52,7 @@ public class SavingsAccountHelper { private final RequestSpecification requestSpec; private final ResponseSpecification responseSpec; + private static final Gson GSON = new JSON().getGson(); private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountHelper.class); private static final String SAVINGS_ACCOUNT_URL = "/fineract-provider/api/v1/savingsaccounts"; @@ -646,6 +650,15 @@ public class SavingsAccountHelper { return Utils.performServerGet(requestSpec, responseSpec, URL, ""); } + public SavingsAccountTransactionsSearchResponse searchTransactions(Integer savingsId, + PagedRequestSavingsTransactionSearch searchReqeust) { + final String SAVINGS_TRANSACTIONS_SEARCH_URL = SAVINGS_ACCOUNT_URL + "/" + savingsId + "/transactions/search" + "?" + + Utils.TENANT_IDENTIFIER; + String jsonBodyToSend = GSON.toJson(searchReqeust); + String response = Utils.performServerPost(this.requestSpec, this.responseSpec, SAVINGS_TRANSACTIONS_SEARCH_URL, jsonBodyToSend); + return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class); + } + public List getSavingsTransactions(final Integer savingsID) { final Object get = getSavingsCollectionAttribute(savingsID, "transactions"); final String json = new Gson().toJson(get);