diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandler.java index 448b438df..c7e3cb62c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandler.java @@ -177,6 +177,7 @@ public final class ErrorHandler { msg = defaultMsg == null ? cause.getMessage() : defaultMsg; if (nre instanceof NonTransientDataAccessException) { msgCode = msgCode == null ? codePfx + ".data.integrity.issue" : msgCode; + log.warn("Handled exception is", nre); return new PlatformDataIntegrityException(msgCode, msg, param, args); } else if (cause instanceof OptimisticLockException) { return (RuntimeException) cause; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java index 37ee9b1f0..81ff3a0fe 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java @@ -22,28 +22,29 @@ import static java.lang.String.format; import jakarta.validation.constraints.NotNull; import java.math.BigInteger; +import java.sql.SQLException; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.logging.log4j.util.Strings; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class DatabaseSpecificSQLGenerator { private final DatabaseTypeResolver databaseTypeResolver; + private final RoutingDataSource dataSource; public static final String SELECT_CLAUSE = "SELECT %s"; - - @Autowired - public DatabaseSpecificSQLGenerator(DatabaseTypeResolver databaseTypeResolver) { - this.databaseTypeResolver = databaseTypeResolver; - } + public static final int IN_CLAUSE_MAX_PARAMS = 10_000; public DatabaseType getDialect() { return databaseTypeResolver.databaseType(); @@ -298,4 +299,67 @@ public class DatabaseSpecificSQLGenerator { } + /** + * Builds an SQL fragment for filtering a column by a list of IDs in a dialect-specific way. + *

+ * For PostgreSQL: + *

+ * For MySQL: + * + * + * @param column + * the name of the column to filter on (e.g. {@code "id"}) + * @param ids + * the list of IDs to include in the condition; must not be empty + * @return an SQL fragment representing the {@code IN} condition, ready to be appended to a query + */ + public String in(String column, List ids) { + return switch (getDialect()) { + case POSTGRESQL -> column + " = ANY (?)"; + case MYSQL -> { + String inSql = String.join(",", Collections.nCopies(ids.size(), "?")); + yield column + " IN (" + inSql + ")"; + } + }; + } + + /** + * Provides the bind parameter values corresponding to the SQL fragment generated by {@link #in(String, List)}. + *

+ * For PostgreSQL: + *

+ * For MySQL: + * + * + * @param ids + * the list of IDs to be bound; must not be empty + * @return an array of parameter values to bind in the same order as the placeholders + * @throws RuntimeException + * if PostgreSQL array creation fails due to a {@link java.sql.SQLException} + */ + public Object[] inParametersFor(List ids) { + return switch (getDialect()) { + case POSTGRESQL -> { + try { + yield new Object[] { DataSourceUtils.getConnection(dataSource).createArrayOf("bigint", ids.toArray(new Long[0])) }; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + case MYSQL -> ids.toArray(); + }; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/util/StreamUtil.java b/fineract-core/src/main/java/org/apache/fineract/util/StreamUtil.java new file mode 100644 index 000000000..6a327f3e7 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/util/StreamUtil.java @@ -0,0 +1,66 @@ +/** + * 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.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +public final class StreamUtil { + + private StreamUtil() {} + + public static Collector foldLeft(final B init, final BiFunction f) { + return Collectors.collectingAndThen(Collectors.reducing(Function.identity(), a -> b -> f.apply(b, a), Function::andThen), + endo -> endo.apply(init)); + } + + /** + * Collector that merges a stream of maps (with list values) into a single map. + *

+ * If the same key appears in multiple maps, the lists are concatenated. + * + * Example: + * + *

{@code
+     *
+     * Map> merged = streamOfMaps.collect(StreamUtils.mergeMapsOfLists());
+     * }
+ * + * @param + * the type of map keys + * @param + * the type of elements in the value lists + * @return a collector producing a merged map with concatenated list values + */ + public static Collector>, ?, Map>> mergeMapsOfLists() { + return Collectors.collectingAndThen(Collectors.flatMapping((Map> m) -> m.entrySet().stream(), + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (list1, list2) -> { + List merged = new ArrayList<>(list1); + merged.addAll(list2); + return merged; + })), HashMap::new // ensures the result is mutable + ); + } +} diff --git a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGeneratorTest.java b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGeneratorTest.java index 57eb471bd..37e68d58b 100644 --- a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGeneratorTest.java +++ b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGeneratorTest.java @@ -25,7 +25,9 @@ import org.mockito.Mockito; public class DatabaseSpecificSQLGeneratorTest { private final DatabaseTypeResolver databaseTypeResolver = Mockito.mock(DatabaseTypeResolver.class); - private final DatabaseSpecificSQLGenerator databaseSpecificSQLGenerator = new DatabaseSpecificSQLGenerator(databaseTypeResolver); + private final RoutingDataSource dataSource = Mockito.mock(RoutingDataSource.class); + private final DatabaseSpecificSQLGenerator databaseSpecificSQLGenerator = new DatabaseSpecificSQLGenerator(databaseTypeResolver, + dataSource); @Test public void testCountQueryResultOnEmptyString() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java index 32c175deb..608799270 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java @@ -33,6 +33,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable { private final Long id; + private final Long loanId; private final LocalDate expectedDisbursementDate; private final LocalDate actualDisbursementDate; private final BigDecimal principal; @@ -61,6 +62,7 @@ public final class DisbursementData implements LoanPrincipalRelatedDataHolder, C this.note = ""; this.linkAccountId = linkAccountId; this.id = null; + this.loanId = null; this.expectedDisbursementDate = null; this.principal = null; this.loanChargeId = null; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java index f25d96b75..c4997ca9d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/RepaymentScheduleRelatedLoanData.java @@ -77,7 +77,7 @@ public class RepaymentScheduleRelatedLoanData { public DisbursementData disbursementData() { BigDecimal waivedChargeAmount = null; - return new DisbursementData(null, this.expectedDisbursementDate, this.actualDisbursementDate, this.principal, + return new DisbursementData(null, null, this.expectedDisbursementDate, this.actualDisbursementDate, this.principal, this.netDisbursalAmount, null, null, waivedChargeAmount); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanDisbursementDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanDisbursementDetails.java index 08dc564b8..31b065ba6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanDisbursementDetails.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanDisbursementDetails.java @@ -122,8 +122,8 @@ public class LoanDisbursementDetails extends AbstractPersistableCustom { public DisbursementData toData() { LocalDate expectedDisburseDate = expectedDisbursementDateAsLocalDate(); BigDecimal waivedChargeAmount = null; - return new DisbursementData(getId(), expectedDisburseDate, this.actualDisbursementDate, this.principal, this.netDisbursalAmount, - null, null, waivedChargeAmount); + return new DisbursementData(getId(), loan.getId(), expectedDisburseDate, this.actualDisbursementDate, this.principal, + this.netDisbursalAmount, null, null, waivedChargeAmount); } public void updateActualDisbursementDate(LocalDate actualDisbursementDate) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 9dc0ea794..533b98c07 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -2030,7 +2030,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch } else { if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { loanApplicationTerms.getDisbursementDatas() - .add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(), + .add(new DisbursementData(1L, null, loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null, null, null)); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanMapper.java index 5a226c849..3f00fea29 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanMapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanMapper.java @@ -92,8 +92,9 @@ public class LoanMapper { actualDisbursementDate = loanDisbursementDetails.actualDisbursementDate(); } BigDecimal waivedChargeAmount = null; - disbursementData.add(new DisbursementData(loanDisbursementDetails.getId(), expectedDisbursementDate, actualDisbursementDate, - loanDisbursementDetails.principal(), loan.getNetDisbursalAmount(), null, null, waivedChargeAmount)); + disbursementData.add( + new DisbursementData(loanDisbursementDetails.getId(), loan.getId(), expectedDisbursementDate, actualDisbursementDate, + loanDisbursementDetails.principal(), loan.getNetDisbursalAmount(), null, null, waivedChargeAmount)); } return disbursementData; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 7abd18294..87bedcb3a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -22,6 +22,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.Page; @@ -104,6 +105,8 @@ public interface LoanReadPlatformService { Collection retrieveLoanDisbursementDetails(Long loanId); + Map> retrieveLoanDisbursementDetails(List loanIds); + DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disbursementId); LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index 8f9d23b3d..5e726aa7a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -281,7 +281,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { loanApplicationTerms.getDisbursementDatas() - .add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(), + .add(new DisbursementData(1L, null, loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null, null, null)); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/StreamUtil.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepository.java similarity index 58% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/StreamUtil.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepository.java index 725a99d82..4029d0c56 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/StreamUtil.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepository.java @@ -16,19 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.infrastructure.core.service; +package org.apache.fineract.portfolio.loanaccount.repository; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collector; -import java.util.stream.Collectors; +import java.util.List; +import java.util.Map; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; -public final class StreamUtil { +public interface CustomizedLoanCapitalizedIncomeBalanceRepository { - private StreamUtil() {} - - public static Collector foldLeft(final B init, final BiFunction f) { - return Collectors.collectingAndThen(Collectors.reducing(Function.identity(), a -> b -> f.apply(b, a), Function::andThen), - endo -> endo.apply(init)); - } + Map> findRepaymentPeriodDataByLoanIds(List loanIds); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepositoryImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepositoryImpl.java new file mode 100644 index 000000000..43386822b --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/CustomizedLoanCapitalizedIncomeBalanceRepositoryImpl.java @@ -0,0 +1,58 @@ +/** + * 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.repository; + +import com.google.common.collect.Lists; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; +import org.apache.fineract.util.StreamUtil; + +@RequiredArgsConstructor +public class CustomizedLoanCapitalizedIncomeBalanceRepositoryImpl implements CustomizedLoanCapitalizedIncomeBalanceRepository { + + private final EntityManager entityManager; + + @Override + public Map> findRepaymentPeriodDataByLoanIds(List loanIds) { + List> partitions = Lists.partition(loanIds, DatabaseSpecificSQLGenerator.IN_CLAUSE_MAX_PARAMS); + return partitions.stream().map(this::doFindRepaymentPeriodDataByLoanIds).collect(StreamUtil.mergeMapsOfLists()); + } + + private Map> doFindRepaymentPeriodDataByLoanIds(List loanIds) { + // making the List serializable since sometimes it's just not + // Caused by: java.lang.IllegalArgumentException: You have attempted to set a value of type + // class java.util.ImmutableCollections$SubList for parameter loanIds with expected type of + // interface java.io.Serializable from query string ... + loanIds = new ArrayList<>(loanIds); + + TypedQuery query = entityManager.createQuery( + LoanCapitalizedIncomeBalanceRepository.FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id IN (:loanIds)", + LoanTransactionRepaymentPeriodData.class); + query.setParameter("loanIds", loanIds); + List result = query.getResultList(); + return result.stream().collect(Collectors.groupingBy(LoanTransactionRepaymentPeriodData::getLoanId)); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java index 0ba8a15fc..a5f70f029 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java @@ -26,8 +26,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; -public interface LoanCapitalizedIncomeBalanceRepository - extends JpaRepository, JpaSpecificationExecutor { +public interface LoanCapitalizedIncomeBalanceRepository extends JpaRepository, + JpaSpecificationExecutor, CustomizedLoanCapitalizedIncomeBalanceRepository { String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lcib.loanTransaction.id, lcib.loan.id, lcib.loanTransaction.dateOf, lcib.loanTransaction.reversed, lcib.amount, lcib.unrecognizedAmount, lcib.loanTransaction.feeChargesPortion) FROM LoanCapitalizedIncomeBalance lcib "; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReportingProcessService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReportingProcessService.java index c1de71c19..cce7fe0a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReportingProcessService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReportingProcessService.java @@ -27,7 +27,6 @@ import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; -import org.apache.fineract.infrastructure.core.service.StreamUtil; import org.apache.fineract.infrastructure.dataqueries.api.RunreportsApiResource; import org.apache.fineract.infrastructure.dataqueries.data.ReportExportType; import org.apache.fineract.infrastructure.dataqueries.service.export.DatatableReportExportService; @@ -35,6 +34,7 @@ import org.apache.fineract.infrastructure.dataqueries.service.export.ResponseHol import org.apache.fineract.infrastructure.report.annotation.ReportService; import org.apache.fineract.infrastructure.report.service.AbstractReportingProcessService; import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.util.StreamUtil; import org.springframework.stereotype.Service; @Service 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 f8612e6ec..5861a8d7a 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 @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.api; +import static java.util.Collections.emptyList; import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.interestType; import com.google.gson.JsonElement; @@ -130,7 +131,9 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData; import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalanceWithLoanId; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; @@ -505,22 +508,37 @@ public class LoansApiResource { final Page loanBasicDetails = this.loanReadPlatformService.retrieveAll(searchParameters); final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); if (associationParameters.contains(DataTableApiConstant.summaryAssociateParamName)) { + + List loanIds = loanBasicDetails.getPageItems().stream().map(LoanAccountData::getId).toList(); + Map> disbursementDataByLoanIds = loanReadPlatformService.retrieveLoanDisbursementDetails(loanIds); + Map> repaymentPeriodDataByLoanIds = loanCapitalizedIncomeBalanceRepository + .findRepaymentPeriodDataByLoanIds(loanIds); + Map> loanTransactionBalancesByLoanIds = loanSummaryBalancesRepository + .retrieveLoanSummaryBalancesByTransactionType(loanIds, LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES); + loanBasicDetails.getPageItems().forEach(i -> { if (i.getSummary() != null) { - Collection disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId()); - List capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository - .findRepaymentPeriodDataByLoanId(i.getId()); - final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData( + Long loanId = i.getId(); + List disbursementData = disbursementDataByLoanIds.getOrDefault(loanId, emptyList()); + List capitalizedIncomeData = repaymentPeriodDataByLoanIds.getOrDefault(loanId, + emptyList()); + List loanTransactionBalances = loanTransactionBalancesByLoanIds.getOrDefault(loanId, + emptyList()); + + 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(), + LoanScheduleData repaymentSchedule = loanReadPlatformService.retrieveRepaymentSchedule(loanId, repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, 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))); + + LoanSummaryData summaryData = loanSummaryDataProvider.withTransactionAmountsSummary(loanId, i.getSummary(), + repaymentSchedule, loanTransactionBalances); + + i.setSummary(summaryData); } }); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index a865fe843..89318fcc7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -637,8 +637,8 @@ public class LoanScheduleAssembler { .getAsBigDecimal(); } BigDecimal waivedChargeAmount = null; - disbursementDatas.add(new DisbursementData(null, expectedDisbursementDate, null, principal, netDisbursalAmount, null, - null, waivedChargeAmount)); + disbursementDatas.add(new DisbursementData(null, null, expectedDisbursementDate, null, principal, netDisbursalAmount, + null, null, waivedChargeAmount)); i++; } while (i < disbursementDataArray.size()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 706077e62..7ab92d157 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -1408,7 +1409,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa Integer loanTermInDays = 0; Set disbursementPeriodIds = new HashSet<>(); while (rs.next()) { - final Integer period = JdbcSupport.getInteger(rs, "period"); LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromDate"); final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); @@ -1941,10 +1941,16 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa @Override public Collection retrieveLoanDisbursementDetails(final Long loanId) { + return retrieveLoanDisbursementDetails(List.of(loanId)).getOrDefault(loanId, Collections.emptyList()); + } + + @Override + public Map> retrieveLoanDisbursementDetails(final List loanIds) { + Object[] parameters = sqlGenerator.inParametersFor(loanIds); final LoanDisbursementDetailMapper rm = new LoanDisbursementDetailMapper(sqlGenerator); - final String sql = "select " + rm.schema() - + " where dd.loan_id=? and dd.is_reversed=false group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date,dd.disbursedon_date,dd.id"; - return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR + final String sql = "select " + rm.schema() + " where " + sqlGenerator.in("dd.loan_id", loanIds) + + " and dd.is_reversed=false group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date,dd.disbursedon_date,dd.id"; + return this.jdbcTemplate.query(sql, rm, parameters).stream().collect(Collectors.groupingBy(DisbursementData::getLoanId)); // NOSONAR } private static final class LoanDisbursementDetailMapper implements RowMapper { @@ -1956,7 +1962,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa } public String schema() { - return "dd.id as id,dd.expected_disburse_date as expectedDisbursementdate, dd.disbursedon_date as actualDisbursementdate,dd.principal as principal,dd.net_disbursal_amount as netDisbursalAmount,sum(lc.amount) chargeAmount, lc.amount_waived_derived waivedAmount, " + return "dd.id as id, dd.loan_id as loanId, dd.expected_disburse_date as expectedDisbursementdate, dd.disbursedon_date as actualDisbursementdate,dd.principal as principal,dd.net_disbursal_amount as netDisbursalAmount,sum(lc.amount) chargeAmount, lc.amount_waived_derived waivedAmount, " + sqlGenerator.groupConcat("lc.id") + " loanChargeId " + "from m_loan l inner join m_loan_disbursement_detail dd on dd.loan_id = l.id left join m_loan_tranche_disbursement_charge tdc on tdc.disbursement_detail_id=dd.id " + "left join m_loan_charge lc on lc.id=tdc.loan_charge_id and lc.is_active=true"; @@ -1965,6 +1971,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa @Override public DisbursementData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { final Long id = rs.getLong("id"); + final Long loanId = rs.getLong("loanId"); final LocalDate expectedDisbursementdate = JdbcSupport.getLocalDate(rs, "expectedDisbursementdate"); final LocalDate actualDisbursementdate = JdbcSupport.getLocalDate(rs, "actualDisbursementdate"); final BigDecimal principal = rs.getBigDecimal("principal"); @@ -1975,8 +1982,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa if (chargeAmount != null && waivedAmount != null) { chargeAmount = chargeAmount.subtract(waivedAmount); } - return new DisbursementData(id, expectedDisbursementdate, actualDisbursementdate, principal, netDisbursalAmount, loanChargeId, - chargeAmount, waivedAmount); + return new DisbursementData(id, loanId, expectedDisbursementdate, actualDisbursementdate, principal, netDisbursalAmount, + loanChargeId, chargeAmount, waivedAmount); } }