diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java index 5cddec376..e93b656f2 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java @@ -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"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalanceWithLoanId.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalanceWithLoanId.java new file mode 100644 index 000000000..94d5fb505 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalanceWithLoanId.java @@ -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; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java index d32293849..720597967 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java @@ -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 retrieveLoanSummaryBalancesByTransactionType(final Long loanId, + public Collection retrieveLoanSummaryBalancesByTransactionType(final Long loanId, + final List transactionTypes) { + return retrieveLoanSummaryBalancesByTransactionType(Arrays.asList(loanId), transactionTypes).getOrDefault(loanId, + new ArrayList<>()); + } + + public Map> retrieveLoanSummaryBalancesByTransactionType(final List loanIds, final List transactionTypes) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(LoanTransactionBalance.class); + CriteriaQuery query = cb.createQuery(LoanTransactionBalanceWithLoanId.class); Root root = query.from(LoanTransaction.class); @@ -52,7 +61,7 @@ public class LoanSummaryBalancesRepository { Path la = r.get("loan"); List 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 queryToExecute = entityManager.createQuery(query); - return queryToExecute.getResultList(); + TypedQuery queryToExecute = entityManager.createQuery(query); + Map> collect = queryToExecute.getResultList().stream() + .collect(Collectors.groupingBy(LoanTransactionBalanceWithLoanId::getLoanId)); + return collect; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index b3485d178..9dc088cc3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -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 loanBasicDetails = this.loanReadPlatformService.retrieveAll(searchParameters); - + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (associationParameters.contains(DataTableApiConstant.summaryAssociateParamName)) { + loanBasicDetails.getPageItems().forEach(i -> { + Collection 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java index 3949077b8..bc8a9ef15 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java @@ -35,7 +35,7 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr @Override public LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, - Collection loanTransactionBalances) { + Collection 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 loanTransactionBalances, + private static BigDecimal fetchLoanTransactionBalanceByType(final Collection loanTransactionBalances, final LoanTransactionType transactionType) { - final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + final Optional 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 loanTransactionBalances, - final LoanTransactionType transactionType) { - final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + private static BigDecimal fetchLoanTransactionBalanceReversedByType( + final Collection loanTransactionBalances, final LoanTransactionType transactionType) { + final Optional optLoanTransactionBalance = loanTransactionBalances.stream() .filter(balance -> balance.getTransactionType().equals(transactionType) && balance.isReversed() && balance.isManuallyAdjustedOrReversed()) .findFirst(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java index c58bae80f..8f32bfacc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java @@ -68,7 +68,7 @@ public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProv @Override @Transactional(readOnly = true) public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, - LoanScheduleData repaymentSchedule, Collection loanTransactionBalances) { + LoanScheduleData repaymentSchedule, Collection loanTransactionBalances) { Loan loan = null; return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java index aef7f8d4c..6a59289a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java @@ -38,10 +38,10 @@ public interface LoanSummaryDataProvider { LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData); LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, - Collection loanTransactionBalances); + Collection loanTransactionBalances); LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, - Collection loanTransactionBalances); + Collection loanTransactionBalances); boolean accept(String loanProcessingStrategyCode); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index 0d3092ab7..fe54a9146 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -62,14 +62,14 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro @Override @Transactional(readOnly = true) public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, - LoanScheduleData repaymentSchedule, Collection loanTransactionBalances) { + LoanScheduleData repaymentSchedule, Collection 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 loanTransactionBalances) { + Collection loanTransactionBalances) { return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); } diff --git a/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm b/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm index f9e69d46c..7edc91a29 100644 --- a/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm +++ b/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm @@ -11147,6 +11147,19 @@ GET https://DomainName/api/v1/loans/{loanId}
Use account no. of loans to restrict results.
+
associations
+
String optional, + Comma separated list of recurring loan 'associations' (itemized below). +
+
+
+ Associations are just extra pieces of data that you might or might not want to retrieve.
+
+
+
+ 'summary': Gets loan account summarry related information. +
+
externalId
String optional diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 125d70fe6..13f01d17c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -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) diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/LoanApiIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/LoanApiIntegrationTest.java new file mode 100644 index 000000000..edaad6776 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/LoanApiIntegrationTest.java @@ -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)); + }); + } +}