FINERACT-2235: add summary data to loans list api

This commit is contained in:
CsengeSóti
2025-04-17 14:02:22 +02:00
committed by Arnold Gálovics
parent 6455311f6c
commit 12617ea1df
11 changed files with 208 additions and 21 deletions
@@ -40,6 +40,7 @@ public final class DataTableApiConstant {
// associationParameters
public static final String allAssociateParamName = "all";
public static final String repaymentScheduleAssociateParamName = "repaymentSchedule";
public static final String summaryAssociateParamName = "summary";
public static final String originalScheduleAssociateParamName = "originalSchedule";
public static final String transactionsAssociateParamName = "transactions";
public static final String chargesAssociateParamName = "charges";
@@ -0,0 +1,35 @@
/**
* 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.data;
import java.math.BigDecimal;
import lombok.Getter;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
@Getter
public class LoanTransactionBalanceWithLoanId extends LoanTransactionBalance {
private final Long loanId;
public LoanTransactionBalanceWithLoanId(LoanTransactionType transactionType, boolean reversed, boolean manuallyAdjustedOrReversed,
BigDecimal amount, Long loanId) {
super(transactionType, reversed, manuallyAdjustedOrReversed, amount);
this.loanId = loanId;
}
}
@@ -26,11 +26,14 @@ import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.jpa.CriteriaQueryFactory;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalanceWithLoanId;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Repository;
@@ -41,10 +44,16 @@ public class LoanSummaryBalancesRepository {
private final EntityManager entityManager;
private final CriteriaQueryFactory criteriaQueryFactory;
public Collection<LoanTransactionBalance> retrieveLoanSummaryBalancesByTransactionType(final Long loanId,
public Collection<LoanTransactionBalanceWithLoanId> retrieveLoanSummaryBalancesByTransactionType(final Long loanId,
final List<LoanTransactionType> transactionTypes) {
return retrieveLoanSummaryBalancesByTransactionType(Arrays.asList(loanId), transactionTypes).getOrDefault(loanId,
new ArrayList<>());
}
public Map<Long, List<LoanTransactionBalanceWithLoanId>> retrieveLoanSummaryBalancesByTransactionType(final List<Long> loanIds,
final List<LoanTransactionType> transactionTypes) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<LoanTransactionBalance> query = cb.createQuery(LoanTransactionBalance.class);
CriteriaQuery<LoanTransactionBalanceWithLoanId> query = cb.createQuery(LoanTransactionBalanceWithLoanId.class);
Root<LoanTransaction> root = query.from(LoanTransaction.class);
@@ -52,7 +61,7 @@ public class LoanSummaryBalancesRepository {
Path<Loan> la = r.get("loan");
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(la.get("id"), loanId)); // Loan
predicates.add(la.get("id").in(loanIds)); // Loans
predicates.add(r.get("typeOf").in(transactionTypes)); // Transaction Type
return cb.and(predicates.toArray(new Predicate[0]));
@@ -60,12 +69,14 @@ public class LoanSummaryBalancesRepository {
criteriaQueryFactory.applySpecificationToCriteria(root, spec, query);
query.groupBy(root.get("typeOf"), root.get("reversed"), root.get("manuallyAdjustedOrReversed"));
query.select(cb.construct(LoanTransactionBalance.class, root.get("typeOf"), root.get("reversed"),
root.get("manuallyAdjustedOrReversed"), cb.sum(root.get("amount"))));
query.groupBy(root.get("typeOf"), root.get("reversed"), root.get("manuallyAdjustedOrReversed"), root.get("loan").get("id"));
query.select(cb.construct(LoanTransactionBalanceWithLoanId.class, root.get("typeOf"), root.get("reversed"),
root.get("manuallyAdjustedOrReversed"), cb.sum(root.get("amount")), root.get("loan").get("id")));
TypedQuery<LoanTransactionBalance> queryToExecute = entityManager.createQuery(query);
return queryToExecute.getResultList();
TypedQuery<LoanTransactionBalanceWithLoanId> queryToExecute = entityManager.createQuery(query);
Map<Long, List<LoanTransactionBalanceWithLoanId>> collect = queryToExecute.getResultList().stream()
.collect(Collectors.groupingBy(LoanTransactionBalanceWithLoanId::getLoanId));
return collect;
}
}
@@ -478,6 +478,7 @@ public class LoansApiResource {
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("associations") @Parameter(description = "associations") final String associations,
@QueryParam("status") @Parameter(description = "status") final String status) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
@@ -490,7 +491,23 @@ public class LoansApiResource {
.externalId(externalId).offset(offset).limit(limit).orderBy(orderBy).status(status).build();
final Page<LoanAccountData> loanBasicDetails = this.loanReadPlatformService.retrieveAll(searchParameters);
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (associationParameters.contains(DataTableApiConstant.summaryAssociateParamName)) {
loanBasicDetails.getPageItems().forEach(i -> {
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(),
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
i.setSummary(loanSummaryDataProvider.withTransactionAmountsSummary(i.getId(), i.getSummary(), repaymentSchedule,
loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(i.getId(),
LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES)));
});
}
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, loanBasicDetails, LOAN_DATA_PARAMETERS);
}
@@ -35,7 +35,7 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
@Override
public LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances) {
Collection<? extends LoanTransactionBalance> loanTransactionBalances) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
BigDecimal totalMerchantRefund = BigDecimal.ZERO;
@@ -137,16 +137,16 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
.totalUnpaidPayableNotDueInterest(totalUnpaidPayableNotDueInterest).totalInterestRefund(totalInterestRefund).build();
}
private static BigDecimal fetchLoanTransactionBalanceByType(final Collection<LoanTransactionBalance> loanTransactionBalances,
private static BigDecimal fetchLoanTransactionBalanceByType(final Collection<? extends LoanTransactionBalance> loanTransactionBalances,
final LoanTransactionType transactionType) {
final Optional<LoanTransactionBalance> optLoanTransactionBalance = loanTransactionBalances.stream()
final Optional<? extends LoanTransactionBalance> optLoanTransactionBalance = loanTransactionBalances.stream()
.filter(balance -> balance.getTransactionType().equals(transactionType) && !balance.isReversed()).findFirst();
return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO;
}
private static BigDecimal fetchLoanTransactionBalanceReversedByType(final Collection<LoanTransactionBalance> loanTransactionBalances,
final LoanTransactionType transactionType) {
final Optional<LoanTransactionBalance> optLoanTransactionBalance = loanTransactionBalances.stream()
private static BigDecimal fetchLoanTransactionBalanceReversedByType(
final Collection<? extends LoanTransactionBalance> loanTransactionBalances, final LoanTransactionType transactionType) {
final Optional<? extends LoanTransactionBalance> optLoanTransactionBalance = loanTransactionBalances.stream()
.filter(balance -> balance.getTransactionType().equals(transactionType) && balance.isReversed()
&& balance.isManuallyAdjustedOrReversed())
.findFirst();
@@ -68,7 +68,7 @@ public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProv
@Override
@Transactional(readOnly = true)
public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData,
LoanScheduleData repaymentSchedule, Collection<LoanTransactionBalance> loanTransactionBalances) {
LoanScheduleData repaymentSchedule, Collection<? extends LoanTransactionBalance> loanTransactionBalances) {
Loan loan = null;
return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances);
}
@@ -38,10 +38,10 @@ public interface LoanSummaryDataProvider {
LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData);
LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances);
Collection<? extends LoanTransactionBalance> loanTransactionBalances);
LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances);
Collection<? extends LoanTransactionBalance> loanTransactionBalances);
boolean accept(String loanProcessingStrategyCode);
}
@@ -62,14 +62,14 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
@Override
@Transactional(readOnly = true)
public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData,
LoanScheduleData repaymentSchedule, Collection<LoanTransactionBalance> loanTransactionBalances) {
LoanScheduleData repaymentSchedule, Collection<? extends LoanTransactionBalance> loanTransactionBalances) {
final Loan loan = loanRepository.findOneWithNotFoundDetection(loanId, true);
return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances);
}
@Override
public LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances) {
Collection<? extends LoanTransactionBalance> loanTransactionBalances) {
return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances);
}
@@ -11147,6 +11147,19 @@ GET https://DomainName/api/v1/loans/{loanId}
</dd>
<dd>Use account no. of loans to restrict results.</dd>
<dt>associations</dt>
<dd>String <span>optional,</span>
<span>Comma separated list of recurring loan 'associations' (itemized below).</span>
</dd>
<dd>
<br>
Associations are just extra pieces of data that you might or might not want to retrieve.<br>
<br>
</dd>
<dd>
<b>'summary':</b> Gets loan account summarry related information.
</dd>
<dt>externalId</dt>
<dd>
String <span>optional</span>
@@ -64,6 +64,7 @@ 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.GetLoansResponse;
import org.apache.fineract.client.models.PaymentTypeData;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -1607,6 +1608,11 @@ public class LoanTransactionHelper {
excludedTransactionTypes, page, size, sort));
}
public GetLoansResponse retrieveAllLoans(final String accountNumber, final String associations) {
return Calls.ok(
FineractClientHelper.getFineractClient().loans.retrieveAll27(null, 0, 10, null, null, accountNumber, associations, null));
}
// 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)
@@ -0,0 +1,104 @@
/**
* 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.loan;
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.junit.jupiter.api.Test;
public class LoanApiIntegrationTest extends BaseLoanIntegrationTest {
@Test
public void test_retrieveLoansWithSummary_Works() {
AtomicLong createdLoanId = new AtomicLong();
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
int numberOfRepayments = 3;
int repaymentEvery = 1;
// Create Loan Product
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
.numberOfRepayments(numberOfRepayments) //
.repaymentEvery(repaymentEvery) //
.installmentAmountInMultiplesOf(null) //
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
.interestType(InterestType.DECLINING_BALANCE)//
.interestRatePerPeriod(10.0)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.isInterestRecalculationEnabled(true)//
.recalculationRestFrequencyInterval(1)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
.allowPartialPeriodInterestCalcualtion(false)//
.disallowExpectedDisbursements(false)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedNumber(null)//
.overAppliedCalculationType(null)//
.multiDisburseLoan(null);//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
Long loanProductId = loanProductResponse.getResourceId();
// Apply and Approve Loan
double amount = 5000.0;
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
.repaymentEvery(repaymentEvery)//
.interestRatePerPeriod(BigDecimal.valueOf(10.0))//
.loanTermFrequency(numberOfRepayments)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);//
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, "01 January 2023"));
Long loanId = approvedLoanResult.getLoanId();
createdLoanId.getAndSet(loanId);
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 January 2023");
});
runAt("01 February 2023", () -> {
long loanId = createdLoanId.get();
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoanDetails(loanId);
GetLoansResponse loansLoanIdResponse = loanTransactionHelper.retrieveAllLoans(loanResponse.getAccountNo(), "summary");
BigDecimal totalUnpaidPayableDueInterest = loansLoanIdResponse.getPageItems().iterator().next().getSummary()
.getTotalUnpaidPayableDueInterest();
assertThat(totalUnpaidPayableDueInterest).isEqualByComparingTo(BigDecimal.valueOf(509.59));
});
}
}