From 221f80699fbb54eabff46361615a28a5787f5d08 Mon Sep 17 00:00:00 2001 From: Attila Budai Date: Sun, 25 Jan 2026 16:55:47 +0100 Subject: [PATCH] FINERACT-2418: fetch loan originators by loan --- fineract-loan-origination/dependencies.gradle | 1 + .../api/LoanOriginatorsApiResource.java | 93 +++++++++++++++++++ .../data/LoanOriginatorsResponse.java | 41 ++++++++ .../LoanOriginatorMappingRepository.java | 9 ++ .../LoanOriginatorReadPlatformService.java | 2 + ...LoanOriginatorReadPlatformServiceImpl.java | 13 +++ 6 files changed, 159 insertions(+) create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorsResponse.java diff --git a/fineract-loan-origination/dependencies.gradle b/fineract-loan-origination/dependencies.gradle index bd2a15867..dee9b9db4 100644 --- a/fineract-loan-origination/dependencies.gradle +++ b/fineract-loan-origination/dependencies.gradle @@ -25,6 +25,7 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) + implementation(project(path: ':fineract-loan')) implementation( 'org.springframework.boot:spring-boot-starter-web', diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java new file mode 100644 index 000000000..4169e60d0 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java @@ -0,0 +1,93 @@ +/** + * 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.loanorigination.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorsResponse; +import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorReadPlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Path("/v1/loans") +@Component +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +@Tag(name = "Loan Originators", description = "Fetch loan originator details for a specific loan") +@RequiredArgsConstructor +public class LoanOriginatorsApiResource { + + private static final String LOAN_RESOURCE_NAME = "LOAN"; + + private final PlatformSecurityContext context; + private final LoanReadPlatformService loanReadPlatformService; + private final LoanOriginatorReadPlatformService loanOriginatorReadPlatformService; + + @GET + @Path("{loanId}/originators") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve originators for a loan by loan ID", description = "Retrieves all originators attached to a specific loan. Requires READ_LOAN permission.") + @ApiResponse(responseCode = "200", description = "OK - Returns wrapped list of originators (may be empty)") + @ApiResponse(responseCode = "403", description = "Insufficient permissions") + @ApiResponse(responseCode = "404", description = "Loan not found") + public LoanOriginatorsResponse retrieveOriginatorsByLoanId(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId) { + this.context.authenticatedUser().validateHasReadPermission(LOAN_RESOURCE_NAME); + + if (!this.loanReadPlatformService.existsByLoanId(loanId)) { + throw new LoanNotFoundException(loanId); + } + + return LoanOriginatorsResponse.of(this.loanOriginatorReadPlatformService.retrieveByLoanId(loanId)); + } + + @GET + @Path("external-id/{loanExternalId}/originators") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve originators for a loan by loan external ID", description = "Retrieves all originators attached to a specific loan using loan external ID. Requires READ_LOAN permission.") + @ApiResponse(responseCode = "200", description = "OK - Returns wrapped list of originators (may be empty)") + @ApiResponse(responseCode = "403", description = "Insufficient permissions") + @ApiResponse(responseCode = "404", description = "Loan not found") + public LoanOriginatorsResponse retrieveOriginatorsByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId) { + this.context.authenticatedUser().validateHasReadPermission(LOAN_RESOURCE_NAME); + + final ExternalId externalId = ExternalIdFactory.produce(loanExternalId); + final Long loanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(externalId); + if (loanId == null) { + throw new LoanNotFoundException(externalId); + } + + return LoanOriginatorsResponse.of(this.loanOriginatorReadPlatformService.retrieveByLoanId(loanId)); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorsResponse.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorsResponse.java new file mode 100644 index 000000000..9f0f1b281 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorsResponse.java @@ -0,0 +1,41 @@ +/** + * 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.loanorigination.data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoanOriginatorsResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private List originators; + + public static LoanOriginatorsResponse of(List originators) { + return new LoanOriginatorsResponse(originators); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java index c3e6e1af3..257029fc1 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java @@ -39,4 +39,13 @@ public interface LoanOriginatorMappingRepository boolean existsByLoanIdAndOriginatorId(Long loanId, Long originatorId); void deleteByLoanIdAndOriginatorId(Long loanId, Long originatorId); + + @org.springframework.data.jpa.repository.Query(""" + SELECT m FROM LoanOriginatorMapping m + JOIN FETCH m.originator o + LEFT JOIN FETCH o.originatorType + LEFT JOIN FETCH o.channelType + WHERE m.loanId = :loanId + """) + List findByLoanIdWithOriginator(@org.springframework.data.repository.query.Param("loanId") Long loanId); } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformService.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformService.java index e7a169bf8..cace22688 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformService.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformService.java @@ -30,4 +30,6 @@ public interface LoanOriginatorReadPlatformService { LoanOriginatorData retrieveByExternalId(String externalId); Long resolveIdByExternalId(String externalId); + + List retrieveByLoanId(Long loanId); } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java index 991385876..b2dd31ff3 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java @@ -18,11 +18,14 @@ */ package org.apache.fineract.portfolio.loanorigination.service; +import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorRepository; import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException; import org.apache.fineract.portfolio.loanorigination.mapper.LoanOriginatorMapper; @@ -37,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; public class LoanOriginatorReadPlatformServiceImpl implements LoanOriginatorReadPlatformService { private final LoanOriginatorRepository loanOriginatorRepository; + private final LoanOriginatorMappingRepository loanOriginatorMappingRepository; private final LoanOriginatorMapper loanOriginatorMapper; @Override @@ -65,4 +69,13 @@ public class LoanOriginatorReadPlatformServiceImpl implements LoanOriginatorRead .orElseThrow(() -> new LoanOriginatorNotFoundException(externalId)); return originator.getId(); } + + @Override + public List retrieveByLoanId(final Long loanId) { + final List mappings = this.loanOriginatorMappingRepository.findByLoanIdWithOriginator(loanId); + if (mappings.isEmpty()) { + return Collections.emptyList(); + } + return mappings.stream().map(LoanOriginatorMapping::getOriginator).map(this.loanOriginatorMapper::toData).toList(); + } }