FINERACT-2203: LoanTransactionsApi add getLoanTransactions
This commit is contained in:
+107
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+162
@@ -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"
|
||||
|
||||
+56
@@ -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, //
|
||||
}
|
||||
}
|
||||
+7
@@ -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 {
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
-17
@@ -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<>();
|
||||
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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);
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+109
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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);
|
||||
}
|
||||
|
||||
+30
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+89
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user