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);