FINERACT-2203: LoanTransactionsApi add getLoanTransactions

This commit is contained in:
adam.magyari
2025-03-19 17:38:19 +01:00
committed by Adam Saghy
parent a8bbba0b24
commit 2cfc0b2503
18 changed files with 763 additions and 19 deletions
@@ -0,0 +1,107 @@
/**
* 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.api.jersey;
import com.google.common.base.Splitter;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.SortDefault;
public class PageableParamProvider implements ValueParamProvider {
@Override
public Function<ContainerRequest, ?> getValueProvider(Parameter parameter) {
if (parameter.getRawType() == Pageable.class && parameter.isAnnotationPresent(Pagination.class)) {
return new PageableFunction(parameter);
}
return null;
}
@Override
public PriorityType getPriority() {
// Use HIGH otherwise it might not be used
return Priority.HIGH;
}
private record PageableFunction(Parameter param) implements Function<ContainerRequest, Pageable> {
@Override
public Pageable apply(ContainerRequest request) {
Pagination pagination = param.getAnnotation(Pagination.class);
MultivaluedMap<String, String> queryParameters = request.getUriInfo().getQueryParameters();
List<Sort.Order> sortingOrders = new ArrayList<>();
AtomicInteger page = new AtomicInteger(0);
int pageSize = pagination.size() > pagination.maximumSize() ? pagination.maximumSize() : pagination.size();
AtomicInteger size = new AtomicInteger(pageSize);
AtomicReference<List<String>> sort = new AtomicReference<>();
queryParameters.forEach((key, list) -> {
switch (key) {
case "page" -> page.set(Integer.parseInt(list.get(0)));
case "size" -> size.set(Integer.parseInt(list.get(0)));
case "sort" -> sort.set(list);
default -> {
}
}
});
if (sort.get() != null) {
for (String propOrder : sort.get()) {
List<String> propOrderSplit = Splitter.on(',').splitToList(propOrder);
String property = propOrderSplit.get(0);
if (propOrderSplit.size() == 1) {
sortingOrders.add(Sort.Order.by(property));
} else {
Sort.Direction direction = Sort.Direction.fromString(propOrderSplit.get(1));
sortingOrders.add(new Sort.Order(direction, property));
}
}
}
if (sortingOrders.isEmpty()) {
if (param.isAnnotationPresent(SortDefault.class)) {
SortDefault sortDefault = param.getAnnotation(SortDefault.class);
for (String sortStr : sortDefault.sort()) {
sortingOrders.add(new Sort.Order(sortDefault.direction(), sortStr));
}
} else if (param.isAnnotationPresent(SortDefault.SortDefaults.class)) {
SortDefault.SortDefaults sortDefaults = param.getAnnotation(SortDefault.SortDefaults.class);
for (SortDefault sortDefault : sortDefaults.value()) {
for (String sortStr : sortDefault.sort()) {
sortingOrders.add(new Sort.Order(sortDefault.direction(), sortStr));
}
}
}
}
return PageRequest.of(page.get(), size.get(), Sort.by(sortingOrders));
}
}
}
@@ -0,0 +1,38 @@
/**
* 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.api.jersey;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pagination {
@AliasFor("size")
int value() default 50;
@AliasFor("value")
int size() default 50;
int maximumSize() default 10000;
}
@@ -896,4 +896,8 @@ public final class ErrorMessageHelper {
public static String wrongExternalID(String actual, String expected) {
return String.format("Wrong transaction External ID - %nActual value is: %s %nExpected value is: %s", actual, expected);
}
public static String wrongValueInTotalPages(Integer actual, Integer expected) {
return String.format("Wrong value for Total pages. %nActual value is: %s %nExpected value is: %s", actual, expected);
}
}
@@ -44,6 +44,8 @@ import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -74,6 +76,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTimeline;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.InterestPauseRequestDto;
import org.apache.fineract.client.models.IsCatchUpRunningResponse;
@@ -3442,4 +3445,163 @@ public class LoanStepDef extends AbstractStepDef {
}
return actualValues;
}
@Then("Log out transaction list by loanId, filtered out the following transaction types: {string}")
public void transactionsExcluded(String excludedTypes) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId();
Response<GetLoansLoanIdTransactionsResponse> transactionsByLoanIdFiltered = getTransactionsByLoanIdFiltered(loanId, excludedTypes);
ErrorHelper.checkSuccessfulApiCall(transactionsByLoanIdFiltered);
List<GetLoansLoanIdTransactionsTransactionIdResponse> transactions = transactionsByLoanIdFiltered.body().getContent();
log.info("Filtered transactions: {}", transactions);
List<String> excludedTypesList = Arrays.stream(excludedTypes.toLowerCase().split(",")).map(String::trim)
.collect(Collectors.toList());
// Verify no transaction with excluded types exists in the filtered list
for (GetLoansLoanIdTransactionsTransactionIdResponse transaction : transactions) {
String transactionType = transaction.getType().getCode();
assertThat(excludedTypesList.contains(transactionType))
.as(String.format("Transaction type '%s' should be excluded but was found in the filtered results", transactionType))
.isFalse();
}
}
@Then("Log out transaction list by loanExternalId, filtered out the following transaction types: {string}")
public void transactionsExcludedByExternalId(String excludedTypes) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
String loanExternalId = loanCreateResponse.body().getResourceExternalId();
Response<GetLoansLoanIdTransactionsResponse> transactionsByLoanExternalIdFiltered = getTransactionsByLoanIExternalIdFiltered(
loanExternalId, excludedTypes);
ErrorHelper.checkSuccessfulApiCall(transactionsByLoanExternalIdFiltered);
List<GetLoansLoanIdTransactionsTransactionIdResponse> transactions = transactionsByLoanExternalIdFiltered.body().getContent();
log.info("Filtered transactions: {}", transactions);
List<String> excludedTypesList = Arrays.stream(excludedTypes.toLowerCase().split(",")).map(String::trim)
.collect(Collectors.toList());
// Verify no transaction with excluded types exists in the filtered list
for (GetLoansLoanIdTransactionsTransactionIdResponse transaction : transactions) {
String transactionType = transaction.getType().getCode();
assertThat(excludedTypesList.contains(transactionType))
.as(String.format("Transaction type '%s' should be excluded but was found in the filtered results", transactionType))
.isFalse();
}
}
@Then("Filtered out transactions list contains the the following entries when filtered out by loanId for transaction types: {string}")
public void transactionsExcludedCheck(String excludedTypes, DataTable table) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdTransactionsResponse> transactionsByLoanIdFiltered = getTransactionsByLoanIdFiltered(loanId, excludedTypes);
ErrorHelper.checkSuccessfulApiCall(transactionsByLoanIdFiltered);
List<GetLoansLoanIdTransactionsTransactionIdResponse> transactions = transactionsByLoanIdFiltered.body().getContent();
List<List<String>> data = table.asLists();
for (int i = 1; i < data.size(); i++) {
List<String> expectedValues = data.get(i);
String transactionDateExpected = expectedValues.get(0);
List<List<String>> actualValuesList = transactions.stream()//
.filter(t -> transactionDateExpected.equals(FORMATTER.format(t.getDate())))//
.map(t -> fetchValuesOfFilteredTransaction(table.row(0), t))//
.collect(Collectors.toList());//
boolean containsExpectedValues = actualValuesList.stream()//
.anyMatch(actualValues -> actualValues.equals(expectedValues));//
assertThat(containsExpectedValues)
.as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, i, actualValuesList, expectedValues)).isTrue();
}
assertThat(transactions.size())
.as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(resourceId, transactions.size(), data.size() - 1))
.isEqualTo(data.size() - 1);
}
@Then("Filtered out transactions list contains the the following entries when filtered out by loanExternalId for transaction types: {string}")
public void transactionsExcludedByLoanExternalIdCheck(String excludedTypes, DataTable table) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
String loanExternalId = loanCreateResponse.body().getResourceExternalId();
long loanId = loanCreateResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdTransactionsResponse> transactionsByLoanExternalIdFiltered = getTransactionsByLoanIExternalIdFiltered(
loanExternalId, excludedTypes);
ErrorHelper.checkSuccessfulApiCall(transactionsByLoanExternalIdFiltered);
List<GetLoansLoanIdTransactionsTransactionIdResponse> transactions = transactionsByLoanExternalIdFiltered.body().getContent();
List<List<String>> data = table.asLists();
for (int i = 1; i < data.size(); i++) {
List<String> expectedValues = data.get(i);
String transactionDateExpected = expectedValues.get(0);
List<List<String>> actualValuesList = transactions.stream()//
.filter(t -> transactionDateExpected.equals(FORMATTER.format(t.getDate())))//
.map(t -> fetchValuesOfFilteredTransaction(table.row(0), t))//
.collect(Collectors.toList());//
boolean containsExpectedValues = actualValuesList.stream()//
.anyMatch(actualValues -> actualValues.equals(expectedValues));//
assertThat(containsExpectedValues)
.as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, i, actualValuesList, expectedValues)).isTrue();
}
assertThat(transactions.size())
.as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(resourceId, transactions.size(), data.size() - 1))
.isEqualTo(data.size() - 1);
}
private Response<GetLoansLoanIdTransactionsResponse> getTransactionsByLoanIdFiltered(Long loanId, String excludedTypes)
throws IOException {
return loanTransactionsApi.retrieveTransactionsByLoanId(loanId, parseExcludedTypes(excludedTypes), null, null, null).execute();
}
private Response<GetLoansLoanIdTransactionsResponse> getTransactionsByLoanIExternalIdFiltered(String loanExternalId,
String excludedTypes) throws IOException {
return loanTransactionsApi.retrieveTransactionsByExternalLoanId(loanExternalId, parseExcludedTypes(excludedTypes), null, null, null)
.execute();
}
public static List<org.apache.fineract.client.models.TransactionType> parseExcludedTypes(String excludedTypes) {
if (excludedTypes == null || excludedTypes.trim().isEmpty()) {
return Collections.emptyList();
} else {
return Arrays.stream(excludedTypes.split(",")).map(String::trim).map(String::toUpperCase)
.map(org.apache.fineract.client.models.TransactionType::valueOf).collect(Collectors.toList());
}
}
private List<String> fetchValuesOfFilteredTransaction(List<String> header, GetLoansLoanIdTransactionsTransactionIdResponse t) {
List<String> actualValues = new ArrayList<>();
for (String headerName : header) {
switch (headerName) {
case "Transaction date" -> actualValues.add(t.getDate() == null ? null : FORMATTER.format(t.getDate()));
case "Transaction Type" -> actualValues.add(t.getType().getCode() == null ? null : t.getType().getCode().substring(20));
case "Amount" -> actualValues.add(t.getAmount() == null ? null : String.valueOf(t.getAmount()));
case "Principal" -> actualValues.add(t.getPrincipalPortion() == null ? null : String.valueOf(t.getPrincipalPortion()));
case "Interest" -> actualValues.add(t.getInterestPortion() == null ? null : String.valueOf(t.getInterestPortion()));
case "Fees" -> actualValues.add(t.getFeeChargesPortion() == null ? null : String.valueOf(t.getFeeChargesPortion()));
case "Penalties" ->
actualValues.add(t.getPenaltyChargesPortion() == null ? null : String.valueOf(t.getPenaltyChargesPortion()));
case "Loan Balance" ->
actualValues.add(t.getOutstandingLoanBalance() == null ? null : String.valueOf(t.getOutstandingLoanBalance()));
case "Overpayment" ->
actualValues.add(t.getOverpaymentPortion() == null ? null : String.valueOf(t.getOverpaymentPortion()));
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
}
}
return actualValues;
}
@Then("Filtered out transactions list has {int} pages in case of size set to {int} and transactions are filtered out for transaction types: {string}")
public void checkPagination(Integer totalPagesExpected, Integer size, String excludedTypes) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId();
Response<GetLoansLoanIdTransactionsResponse> transactionsByLoanIdFiltered = loanTransactionsApi
.retrieveTransactionsByLoanId(loanId, parseExcludedTypes(excludedTypes), null, size, null).execute();
Integer totalPagesActual = transactionsByLoanIdFiltered.body().getTotalPages();
assertThat(totalPagesActual).as(ErrorMessageHelper.wrongValueInTotalPages(totalPagesActual, totalPagesExpected))
.isEqualTo(totalPagesExpected);
}
}
@@ -6038,6 +6038,7 @@ Feature: LoanAccrualActivity
| 01 July 2024 | Repayment | 340.17 | 336.11 | 1.96 | 0.0 | 0.0 | 0.0 | false | false |
| 01 July 2024 | Accrual Activity | 1.96 | 0.0 | 1.96 | 0.0 | 0.0 | 0.0 | false | false |
@TestRailId:C3525
Scenario: Verify accrual activity behavior in case of backdated repayment - UC1
When Admin sets the business date to "01 January 2025"
@@ -6399,3 +6400,52 @@ Feature: LoanAccrualActivity
| 01 February 2025 | Accrual Activity | 4.67 | 0.0 | 4.67 | 0.0 | 0.0 | 0.0 | false | true |
And "Accrual Activity" transaction on "01 February 2025" got reverse-replayed on "02 February 2025"
And External ID of replayed "Accrual Activity" on "01 February 2025" is matching with "saved-external-id"
@TestRailId:C3533
Scenario: Logging out transaction list, excluded given transaction types
When Admin sets the business date to "01 January 2025"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY | 01 January 2025 | 2000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "2000" amount and expected disbursement date on "01 January 2025"
When Admin successfully disburse the loan on "01 January 2025" with "2000" EUR transaction amount
And Admin runs inline COB job for Loan
When Admin sets the business date to "05 January 2025"
And Customer makes "AUTOPAY" repayment on "05 January 2025" with 200 EUR transaction amount
And Admin runs inline COB job for Loan
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 2000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 2000.0 | false | false |
| 01 January 2025 | Accrual | 0.38 | 0.0 | 0.38 | 0.0 | 0.0 | 0.0 | false | false |
| 02 January 2025 | Accrual | 0.37 | 0.0 | 0.37 | 0.0 | 0.0 | 0.0 | false | false |
| 03 January 2025 | Accrual | 0.38 | 0.0 | 0.38 | 0.0 | 0.0 | 0.0 | false | false |
| 04 January 2025 | Accrual | 0.38 | 0.0 | 0.38 | 0.0 | 0.0 | 0.0 | false | false |
| 05 January 2025 | Repayment | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 1800.0 | false | false |
Then Log out transaction list by loanId, filtered out the following transaction types: "disbursement, accrual"
Then Log out transaction list by loanExternalId, filtered out the following transaction types: "accrual"
Then Filtered out transactions list contains the the following entries when filtered out by loanId for transaction types: "accrual"
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 January 2025 | disbursement | 2000.0 | | | | | 2000.0 |
| 05 January 2025 | repayment | 200.0 | 200.0 | | | | 1800.0 |
Then Filtered out transactions list contains the the following entries when filtered out by loanExternalId for transaction types: "accrual"
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 January 2025 | disbursement | 2000.0 | | | | | 2000.0 |
| 05 January 2025 | repayment | 200.0 | 200.0 | | | | 1800.0 |
Then Filtered out transactions list contains the the following entries when filtered out by loanId for transaction types: ""
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 January 2025 | disbursement | 2000.0 | | | | | 2000.0 |
| 01 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 02 January 2025 | accrual | 0.37 | | 0.37 | | | |
| 03 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 04 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 05 January 2025 | repayment | 200.0 | 200.0 | | | | 1800.0 |
Then Filtered out transactions list contains the the following entries when filtered out by loanId for transaction types: "merchant_issued_refund"
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 January 2025 | disbursement | 2000.0 | | | | | 2000.0 |
| 01 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 02 January 2025 | accrual | 0.37 | | 0.37 | | | |
| 03 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 04 January 2025 | accrual | 0.38 | | 0.38 | | | |
| 05 January 2025 | repayment | 200.0 | 200.0 | | | | 1800.0 |
Then Filtered out transactions list has 4 pages in case of size set to 1 and transactions are filtered out for transaction types: "disbursement, repayment"
@@ -0,0 +1,56 @@
/**
* 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.loanaccount.api;
public interface LoanTransactionApiConstants {
enum TransactionType {
disbursement, //
repayment, //
waiver, //
repaymentAtDisbursement, //
writeOff, //
markedForRescheduling, //
recoveryRepayment, //
waiveCharges, //
accrual, //
initiateTransfer, //
approveTransfer, //
withdrawTransfer, //
rejectTransfer, //
refund, //
chargePayment, //
incomePosting, //
creditBalanceRefund, //
merchantIssuedRefund, //
payoutRefund, //
goodwillCredit, //
chargeRefund, //
chargeback, //
chargeAdjustment, //
chargeOff, //
downPayment, //
reAge, //
reAmortize, //
interestPaymentWaiver, //
accrualActivity, //
interestRefund, //
accrualAdjustment, //
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.Set;
import org.springframework.data.domain.Page;
/**
* Created by Chirag Gupta on 12/30/17.
@@ -275,6 +276,12 @@ final class LoanTransactionsApiResourceSwagger {
}
}
@Schema(description = "GetLoansLoanIdTransactionsResponse")
public abstract static class GetLoansLoanIdTransactionsResponse implements Page<GetLoansLoanIdTransactionsTransactionIdResponse> {
private GetLoansLoanIdTransactionsResponse() {}
}
@Schema(description = "PostLoansLoanIdTransactionsRequest")
public static final class PostLoansLoanIdTransactionsRequest {
@@ -29,6 +29,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.springframework.integration.annotation.Default;
/**
* Immutable data object representing a loan transaction.
@@ -213,6 +214,7 @@ public class LoanTransactionData {
}
@Default // Default constructor for mapper
public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount,
final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion,
@@ -288,4 +288,8 @@ public class LoanRepositoryWrapper {
public List<Long> findIdByExternalIds(List<ExternalId> externalIds) {
return repository.findIdsByExternalIds(externalIds);
}
public boolean existsByLoanId(Long loanId) {
return repository.existsById(loanId);
}
}
@@ -44,17 +44,13 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
/**
@@ -724,19 +720,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return !MathUtil.isEmpty(amount);
}
public LoanTransactionData toData(final CurrencyData currencyData, final AccountTransferData transfer) {
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(this.typeOf);
PaymentDetailData paymentDetailData = null;
if (this.paymentDetail != null) {
paymentDetailData = this.paymentDetail.toData();
}
return new LoanTransactionData(getId(), this.office.getId(), this.office.getName(), transactionType, paymentDetailData,
currencyData, getTransactionDate(), this.amount, this.loan.getNetDisbursalAmount(), this.principalPortion,
this.interestPortion, this.feeChargesPortion, this.penaltyChargesPortion, this.overPaymentPortion, this.externalId,
transfer, null, outstandingLoanBalance, this.unrecognizedIncomePortion, this.manuallyAdjustedOrReversed, this.loan.getId(),
this.loan.getExternalId());
}
public Map<String, Object> toMapData(final String currencyCode) {
final Map<String, Object> thisTransactionData = new LinkedHashMap<>();
@@ -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.loanaccount.mapper;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class, uses = { LoanTransactionRelationMapper.class, LoanChargePaidByMapper.class })
public interface LoanTransactionMapper {
@Mapping(target = "numberOfRepayments", ignore = true)
@Mapping(target = "loanRepaymentScheduleInstallments", ignore = true)
@Mapping(target = "writeOffReasonOptions", ignore = true)
@Mapping(target = "chargeOffReasonOptions", ignore = true)
@Mapping(target = "paymentTypeOptions", ignore = true)
@Mapping(target = "overpaymentPortion", ignore = true)
@Mapping(target = "transfer", ignore = true)
@Mapping(target = "fixedEmiAmount", ignore = true)
@Mapping(target = "date", source = "dateOf")
@Mapping(target = "loanChargePaidByList", source = "loanChargesPaid")
@Mapping(target = "manuallyReversed", source = "manuallyAdjustedOrReversed")
@Mapping(target = "transactionRelations", source = "loanTransactionRelations")
@Mapping(target = "officeId", source = "office.id")
@Mapping(target = "officeName", source = "office.name")
@Mapping(target = "loanId", source = "loan.id")
@Mapping(target = "externalLoanId", source = "loan.externalId")
@Mapping(target = "netDisbursalAmount", source = "loan.netDisbursalAmount")
@Mapping(target = "transactionType", expression = "java(org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.transactionType(loanTransaction.getTypeOf()))")
@Mapping(target = "paymentDetailData", expression = "java(loanTransaction.getPaymentDetail() != null ? loanTransaction.getPaymentDetail().toData() : null)")
@Mapping(target = "currency", expression = "java(loanTransaction.getLoan().getCurrency().toData())")
LoanTransactionData mapLoanTransaction(LoanTransaction loanTransaction);
}
@@ -20,11 +20,15 @@
package org.apache.fineract.infrastructure.core.jersey;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Singleton;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.ext.Provider;
import org.apache.fineract.infrastructure.core.api.jersey.PageableParamProvider;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
@@ -35,6 +39,13 @@ public class JerseyConfig extends ResourceConfig {
JerseyConfig() {
register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
register(new AbstractBinder() {
@Override
protected void configure() {
bind(PageableParamProvider.class).to(ValueParamProvider.class).in(Singleton.class);
}
});
property(ServerProperties.WADL_FEATURE_DISABLE, true);
}
@@ -45,12 +45,14 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.DateParam;
import org.apache.fineract.infrastructure.core.api.jersey.Pagination;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.DateFormat;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
@@ -64,12 +66,15 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
@Path("/v1/loans")
@@ -224,6 +229,40 @@ public class LoanTransactionsApiResource {
return retrieveTransaction(null, loanExternalId, null, externalTransactionId, uriInfo);
}
@GET
@Path("{loanId}/transactions")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Transactions", description = "Retrieves transactions of a loan")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsResponse.class))) })
public Page<LoanTransactionData> retrieveTransactionsByLoanId(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@QueryParam("excludedTypes") @Parameter(description = "excludedTypes", in = ParameterIn.QUERY, schema = @Schema(enumAsRef = true)) final List<LoanTransactionApiConstants.TransactionType> excludedTypes,
@QueryParam("page") @Parameter(description = "page") final Integer page,
@QueryParam("size") @Parameter(description = "size") final Integer size,
@QueryParam("sort") @Parameter(description = "sort") final String sort, @Parameter(hidden = true) @Pagination Pageable pageable,
@Context final UriInfo uriInfo) {
return retrieveTransactions(loanId, null, excludedTypes, pageable, uriInfo);
}
@GET
@Path("external-id/{loanExternalId}/transactions")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Transactions", description = "Retrieves transactions of a loan")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsResponse.class))) })
public Page<LoanTransactionData> retrieveTransactionsByExternalLoanId(
@PathParam("loanExternalId") @Parameter(description = "loanId", required = true) final String loanExternalId,
@QueryParam("excludedTypes") @Parameter(description = "excludedTypes", in = ParameterIn.QUERY, schema = @Schema(enumAsRef = true)) final List<LoanTransactionApiConstants.TransactionType> excludedTypes,
@QueryParam("page") @Parameter(description = "page") final Integer page,
@QueryParam("size") @Parameter(description = "size") final Integer size,
@QueryParam("sort") @Parameter(description = "sort") final String sort, @Parameter(hidden = true) @Pagination Pageable pageable,
@Context final UriInfo uriInfo) {
return retrieveTransactions(null, loanExternalId, excludedTypes, pageable, uriInfo);
}
@POST
@Path("{loanId}/transactions")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -439,6 +478,59 @@ public class LoanTransactionsApiResource {
return this.toApiJsonSerializer.serialize(settings, transactionData, this.responseDataParameters);
}
private Page<LoanTransactionData> retrieveTransactions(final Long loanId, final String loanExternalIdStr,
final List<LoanTransactionApiConstants.TransactionType> excludedTypes, Pageable pageable, final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr);
Long resolvedLoanId = getResolvedLoanIdWithExistsCheck(loanId, loanExternalId);
Set<LoanTransactionType> excludedTransactionTypes = new HashSet<>();
if (CollectionUtils.isNotEmpty(excludedTypes)) {
excludedTypes.forEach(x -> excludedTransactionTypes.add(transactionTypeFromParam(x)));
}
return loanReadPlatformService.retrieveLoanTransactions(resolvedLoanId, excludedTransactionTypes, pageable);
}
private LoanTransactionType transactionTypeFromParam(LoanTransactionApiConstants.TransactionType transactionTypeParam) {
return switch (transactionTypeParam) {
case disbursement -> LoanTransactionType.DISBURSEMENT;
case repayment -> LoanTransactionType.REPAYMENT;
case waiver -> LoanTransactionType.WAIVE_INTEREST;
case repaymentAtDisbursement -> LoanTransactionType.REPAYMENT_AT_DISBURSEMENT;
case writeOff -> LoanTransactionType.WRITEOFF;
case markedForRescheduling -> LoanTransactionType.MARKED_FOR_RESCHEDULING;
case recoveryRepayment -> LoanTransactionType.RECOVERY_REPAYMENT;
case waiveCharges -> LoanTransactionType.WAIVE_CHARGES;
case accrual -> LoanTransactionType.ACCRUAL;
case initiateTransfer -> LoanTransactionType.INITIATE_TRANSFER;
case approveTransfer -> LoanTransactionType.APPROVE_TRANSFER;
case withdrawTransfer -> LoanTransactionType.WITHDRAW_TRANSFER;
case rejectTransfer -> LoanTransactionType.REJECT_TRANSFER;
case refund -> LoanTransactionType.REFUND;
case chargePayment -> LoanTransactionType.CHARGE_PAYMENT;
case incomePosting -> LoanTransactionType.INCOME_POSTING;
case creditBalanceRefund -> LoanTransactionType.CREDIT_BALANCE_REFUND;
case merchantIssuedRefund -> LoanTransactionType.MERCHANT_ISSUED_REFUND;
case payoutRefund -> LoanTransactionType.PAYOUT_REFUND;
case goodwillCredit -> LoanTransactionType.GOODWILL_CREDIT;
case chargeRefund -> LoanTransactionType.CHARGE_REFUND;
case chargeback -> LoanTransactionType.CHARGEBACK;
case chargeAdjustment -> LoanTransactionType.CHARGE_ADJUSTMENT;
case chargeOff -> LoanTransactionType.CHARGE_OFF;
case reAge -> LoanTransactionType.REAGE;
case reAmortize -> LoanTransactionType.REAMORTIZE;
case interestPaymentWaiver -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
case accrualActivity -> LoanTransactionType.ACCRUAL_ACTIVITY;
case interestRefund -> LoanTransactionType.INTEREST_REFUND;
case accrualAdjustment -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
default ->
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
};
}
private String executeTransaction(final Long loanId, final String loanExternalIdStr, final String commandParam,
final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
@@ -637,4 +729,21 @@ public class LoanTransactionsApiResource {
}
return resolvedLoanId;
}
private Long getResolvedLoanIdWithExistsCheck(final Long loanId, final ExternalId loanExternalId) {
if (loanId != null) {
if (!loanReadPlatformService.existsByLoanId(loanId)) {
throw new LoanNotFoundException(loanId);
}
return loanId;
} else if (loanExternalId != null) {
final Long resolvedLoanId = loanReadPlatformService.retrieveLoanIdByExternalId(loanExternalId);
if (resolvedLoanId == null) {
throw new LoanNotFoundException(loanExternalId);
}
return resolvedLoanId;
} else {
throw new IllegalArgumentException("loanId and loanExternalId cannot be both null");
}
}
}
@@ -18,9 +18,11 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
@@ -39,6 +41,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.springframework.data.domain.Pageable;
public interface LoanReadPlatformService {
@@ -51,6 +54,9 @@ public interface LoanReadPlatformService {
Collection<LoanTransactionData> retrieveLoanTransactions(Long loanId);
org.springframework.data.domain.Page<LoanTransactionData> retrieveLoanTransactions(@NotNull Long loanId,
Set<LoanTransactionType> excludedTransactionTypes, Pageable pageable);
LoanAccountData retrieveTemplateWithClientAndProductDetails(Long clientId, Long productId);
LoanAccountData retrieveTemplateWithGroupAndProductDetails(Long groupId, Long productId);
@@ -143,4 +149,6 @@ public interface LoanReadPlatformService {
Long retrieveLoanIdByExternalId(ExternalId externalId);
List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds);
boolean existsByLoanId(Long loanId);
}
@@ -21,6 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.service;
import static java.lang.Boolean.TRUE;
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.interestType;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -120,6 +122,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
@@ -134,6 +137,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
@@ -174,6 +178,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final LoanChargePaidByReadService loanChargePaidByReadService;
private final LoanTransactionRelationReadService loanTransactionRelationReadService;
private final LoanForeclosureValidator loanForeclosureValidator;
private final LoanTransactionMapper loanTransactionMapper;
@Override
public LoanAccountData retrieveOne(final Long loanId) {
@@ -598,6 +603,27 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
}
}
@Override
public org.springframework.data.domain.Page<LoanTransactionData> retrieveLoanTransactions(
@jakarta.validation.constraints.NotNull Long loanId, Set<LoanTransactionType> excludedTransactionTypes, Pageable pageable) {
final org.springframework.data.domain.Page<LoanTransaction> transactionPage = loanTransactionRepository
.findAll((root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
final Join<Object, Object> loanjoin = root.join("loan");
predicates.add(builder.equal(loanjoin.get("id"), loanId));
if (excludedTransactionTypes != null && !excludedTransactionTypes.isEmpty()) {
final List<Integer> excludedTransactionTypeValues = excludedTransactionTypes.stream()
.map(LoanTransactionType::getValue).toList();
predicates.add(builder.not(root.get("typeOf").in(excludedTransactionTypeValues)));
}
return builder.and(predicates.toArray(new Predicate[] {}));
}, pageable);
return transactionPage.map(loanTransactionMapper::mapLoanTransaction);
}
@Override
public Long getLoanIdByLoanExternalId(String externalId) {
ExternalId loanExternalId = ExternalIdFactory.produce(externalId);
@@ -2182,4 +2208,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return loanRepositoryWrapper.findIdByExternalIds(externalIds);
}
@Override
public boolean existsByLoanId(Long loanId) {
return loanRepositoryWrapper.existsByLoanId(loanId);
}
}
@@ -85,6 +85,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanSchedu
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
@@ -325,7 +326,7 @@ public class LoanAccountConfiguration {
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator,
DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository,
LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService,
LoanForeclosureValidator loanForeclosureValidator) {
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper) {
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
@@ -333,7 +334,7 @@ public class LoanAccountConfiguration {
loanRepaymentScheduleTransactionProcessorFactory, floatingRatesReadPlatformService, loanUtilService,
configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
loanForeclosureValidator);
loanForeclosureValidator, loanTransactionMapper);
}
@Bean
@@ -0,0 +1,89 @@
/**
* 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 java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.TransactionType;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class LoanTransactionTest extends BaseLoanIntegrationTest {
@Test
public void testGetLoanTransactionsFiltering() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
final String loanExternalIdStr = UUID.randomUUID().toString();
runAt("20 December 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024",
430.0, 7.0, 6, (request) -> request.externalId(loanExternalIdStr));
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024");
});
runAt("21 December 2024", () -> {
executeInlineCOB(loanIdRef.get());
});
runAt("20 January 2025", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "20 January 2025", 82.20);
});
runAt("20 February 2025", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "20 February 2025", 82.20);
});
runAt("23 February 2025", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
final GetLoansLoanIdTransactionsResponse allLoanTransactionsPage = loanTransactionHelper.getLoanTransactions(loanId);
Assertions.assertEquals(67L, allLoanTransactionsPage.getTotalElements());
final GetLoansLoanIdTransactionsResponse nonAccrualLoanTransactionsPage = loanTransactionHelper.getLoanTransactions(loanId,
List.of(TransactionType.ACCRUAL));
Assertions.assertEquals(3L, nonAccrualLoanTransactionsPage.getTotalElements());
final GetLoansLoanIdTransactionsResponse allLoanTransactionsByExternalIdPage = loanTransactionHelper
.getLoanTransactionsByExternalId(loanExternalIdStr);
Assertions.assertEquals(67L, allLoanTransactionsByExternalIdPage.getTotalElements());
final GetLoansLoanIdTransactionsResponse nonAccrualLoanTransactionsByExternalIdPage = loanTransactionHelper
.getLoanTransactionsByExternalId(loanExternalIdStr, List.of(TransactionType.ACCRUAL));
Assertions.assertEquals(3L, nonAccrualLoanTransactionsByExternalIdPage.getTotalElements());
});
}
}
@@ -38,6 +38,7 @@ import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -60,6 +61,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdSummary;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.PaymentTypeData;
@@ -86,6 +88,7 @@ import org.apache.fineract.client.models.PutLoansLoanIdChargesChargeIdRequest;
import org.apache.fineract.client.models.PutLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.PutLoansLoanIdRequest;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.models.TransactionType;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.client.util.Calls;
import org.apache.fineract.client.util.JSON;
@@ -1575,6 +1578,35 @@ public class LoanTransactionHelper {
return Calls.ok(FineractClientHelper.getFineractClient().loans.retrieveLoan1(loanExternalId, false, "all", null, null));
}
public GetLoansLoanIdTransactionsResponse getLoanTransactions(final Long loanId) {
return getLoanTransactions(loanId, Collections.emptyList(), null, null, null);
}
public GetLoansLoanIdTransactionsResponse getLoanTransactions(final Long loanId, List<TransactionType> excludedTransactionTypes) {
return getLoanTransactions(loanId, excludedTransactionTypes, null, null, null);
}
public GetLoansLoanIdTransactionsResponse getLoanTransactions(final Long loanId, List<TransactionType> excludedTransactionTypes,
Integer page, Integer size, String sort) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.retrieveTransactionsByLoanId(loanId,
excludedTransactionTypes, page, size, sort));
}
public GetLoansLoanIdTransactionsResponse getLoanTransactionsByExternalId(final String loanExternalId) {
return getLoanTransactionsByExternalId(loanExternalId, Collections.emptyList(), null, null, null);
}
public GetLoansLoanIdTransactionsResponse getLoanTransactionsByExternalId(final String loanExternalId,
List<TransactionType> excludedTransactionTypes) {
return getLoanTransactionsByExternalId(loanExternalId, excludedTransactionTypes, null, null, null);
}
public GetLoansLoanIdTransactionsResponse getLoanTransactionsByExternalId(final String loanExternalId,
List<TransactionType> excludedTransactionTypes, Integer page, Integer size, String sort) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.retrieveTransactionsByExternalLoanId(loanExternalId,
excludedTransactionTypes, page, size, sort));
}
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)