From 186652c77147d64e41e3f33e627b56d66738ab59 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Wed, 19 Aug 2026 16:58:15 +0200 Subject: [PATCH] FINERACT-2773: Add template API for Asset externalization product attributes --- ...OwnerLoanProductAttributesApiResource.java | 12 +++++ ...rnalTransferLoanProductAttributesData.java | 2 - ...sferLoanProductAttributesTemplateData.java | 39 ++++++++++++++ ...xternalAssetOwnerLoanProductAttribute.java | 8 +++ ...xternalAssetOwnerLoanProductAttribute.java | 29 ++++++++--- ...OwnerLoanProductAttributesReadService.java | 4 ++ ...rLoanProductAttributesReadServiceImpl.java | 51 ++++++++++++++++++- ...LoanProductAttributesWriteServiceImpl.java | 4 +- ...rLoanProductAttributesReadServiceTest.java | 16 ++++++ 9 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesTemplateData.java diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnerLoanProductAttributesApiResource.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnerLoanProductAttributesApiResource.java index a124b10bbe3..2e46287db9d 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnerLoanProductAttributesApiResource.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnerLoanProductAttributesApiResource.java @@ -35,6 +35,7 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; +import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; @@ -45,6 +46,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformUserRightsContext; import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition; import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData; +import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesTemplateData; import org.apache.fineract.investor.service.ExternalAssetOwnerLoanProductAttributesReadService; import org.springframework.context.annotation.Conditional; import org.springframework.stereotype.Component; @@ -60,6 +62,16 @@ public class ExternalAssetOwnerLoanProductAttributesApiResource { private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final ExternalAssetOwnerLoanProductAttributesReadService externalAssetOwnerLoanProductAttributesReadService; + @GET + @Path("/template") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve External Asset Owner Loan Product Attributes Template", operationId = "retrieveTemplateExternalAssetOwnerLoanProductAttributes", description = "Retrieves all available external asset owner loan product attributes and the values each attribute can take.") + public List getExternalAssetOwnerLoanProductAttributesTemplate() { + platformUserRightsContext.isAuthenticated(); + + return externalAssetOwnerLoanProductAttributesReadService.retrieveExternalAssetOwnerLoanProductAttributesTemplate(); + } + @POST @Path("/{loanProductId}/attributes") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesData.java index a1eaee87b2c..0c75154852d 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesData.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesData.java @@ -20,14 +20,12 @@ import java.io.Serializable; import lombok.Data; -import lombok.Getter; /** * Data object representing an external transfer loan product attribute */ @Data -@Getter public class ExternalTransferLoanProductAttributesData implements Serializable { private Long attributeId; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesTemplateData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesTemplateData.java new file mode 100644 index 00000000000..fb0329655c7 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferLoanProductAttributesTemplateData.java @@ -0,0 +1,39 @@ +/** + * 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.investor.data; + +import java.io.Serializable; +import java.util.List; +import lombok.Data; +import lombok.Getter; + +/** + * Data object representing an external transfer loan product attribute + */ + +@Data +@Getter +public class ExternalTransferLoanProductAttributesTemplateData implements Serializable { + + private Long attributeId; + private Long loanProductId; + private String attributeKey; + private List attributeValues; + private boolean isMultiValue; +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/ExternalAssetOwnerLoanProductAttribute.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/ExternalAssetOwnerLoanProductAttribute.java index 99b8a2d2e9e..2782ebfbfd0 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/ExternalAssetOwnerLoanProductAttribute.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/ExternalAssetOwnerLoanProductAttribute.java @@ -18,9 +18,17 @@ */ package org.apache.fineract.investor.data.attribute; +import java.util.List; + public interface ExternalAssetOwnerLoanProductAttribute { String getAttributeKey(); String getAttributeValue(); + + List getAttributeValues(); + + boolean validate(String attributeValue); + + boolean isMultiValue(); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/SettlementModelExternalAssetOwnerLoanProductAttribute.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/SettlementModelExternalAssetOwnerLoanProductAttribute.java index 76d63e32430..a4b96da0363 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/SettlementModelExternalAssetOwnerLoanProductAttribute.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/attribute/SettlementModelExternalAssetOwnerLoanProductAttribute.java @@ -18,17 +18,19 @@ */ package org.apache.fineract.investor.data.attribute; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + public enum SettlementModelExternalAssetOwnerLoanProductAttribute implements ExternalAssetOwnerLoanProductAttribute { - DEFAULT_SETTLEMENT("DEFAULT_SETTLEMENT"), // - DELAYED_SETTLEMENT("DELAYED_SETTLEMENT"); // + DEFAULT_SETTLEMENT, // + DELAYED_SETTLEMENT; // private final String attributeKey; - private final String attributeValue; - SettlementModelExternalAssetOwnerLoanProductAttribute(String attributeValue) { + SettlementModelExternalAssetOwnerLoanProductAttribute() { this.attributeKey = "SETTLEMENT_MODEL"; - this.attributeValue = attributeValue; } @Override @@ -38,6 +40,21 @@ public String getAttributeKey() { @Override public String getAttributeValue() { - return attributeValue; + return name(); + } + + @Override + public List getAttributeValues() { + return Arrays.stream(values()).map(Enum::name).toList(); + } + + @Override + public boolean validate(String attributeValue) { + return this.getAttributeValue().equals(attributeValue.toUpperCase(Locale.ROOT)); + } + + @Override + public boolean isMultiValue() { + return false; } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadService.java index 4f76aeb3c3d..72be74458a2 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadService.java @@ -18,11 +18,15 @@ */ package org.apache.fineract.investor.service; +import java.util.List; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData; +import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesTemplateData; public interface ExternalAssetOwnerLoanProductAttributesReadService { + List retrieveExternalAssetOwnerLoanProductAttributesTemplate(); + Page retrieveAllLoanProductAttributesByLoanProductId(Long loanProductId, String attributeKey); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceImpl.java index 5206043af50..19b3c80f522 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceImpl.java @@ -18,17 +18,24 @@ */ package org.apache.fineract.investor.service; +import static org.reflections.scanners.Scanners.SubTypes; + import jakarta.persistence.criteria.Predicate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData; +import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesTemplateData; +import org.apache.fineract.investor.data.attribute.ExternalAssetOwnerLoanProductAttribute; import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes; import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; +import org.reflections.Reflections; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -41,9 +48,29 @@ @Transactional(readOnly = true) public class ExternalAssetOwnerLoanProductAttributesReadServiceImpl implements ExternalAssetOwnerLoanProductAttributesReadService { + private static final String INVESTOR_PATH = "org.apache.fineract.investor"; + private final ExternalAssetOwnerLoanProductAttributesRepository externalAssetOwnerLoanProductAttributesRepository; private final LoanProductRepository loanProductRepository; private final ExternalAssetOwnerLoanProductAttributesMapper mapper; + private final Set> implementingClasses = new Reflections(INVESTOR_PATH) + .get(SubTypes.of(ExternalAssetOwnerLoanProductAttribute.class).asClass()); + + @Override + public List retrieveExternalAssetOwnerLoanProductAttributesTemplate() { + List result = new ArrayList<>(); + + for (Class implementingClass : implementingClasses) { + if (implementingClass.isEnum()) { + Arrays.stream(implementingClass.getEnumConstants()).map(ExternalAssetOwnerLoanProductAttribute.class::cast) + .forEach(attribute -> addAttributeValues(result, attribute)); + } else { + addAttributeValues(result, createAttribute(implementingClass)); + } + } + + return result; + } @Override @Cacheable(cacheNames = "externalAssetOwnerLoanProductAttributes", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#loanProductId.toString() + #attributeKey)", unless = "#attributeKey == null") @@ -61,6 +88,28 @@ public Page retrieveAllLoanProductAtt pageOfAttributeData.getNumberOfElements()); } + private void addAttributeValues(List result, + ExternalAssetOwnerLoanProductAttribute attribute) { + if (result.stream().anyMatch(data -> data.getAttributeKey().equals(attribute.getAttributeKey()))) { + return; + } + + ExternalTransferLoanProductAttributesTemplateData data = new ExternalTransferLoanProductAttributesTemplateData(); + data.setAttributeKey(attribute.getAttributeKey()); + data.setAttributeValues(attribute.getAttributeValues()); + data.setMultiValue(attribute.isMultiValue()); + result.add(data); + } + + private static ExternalAssetOwnerLoanProductAttribute createAttribute(final Class implementingClass) { + try { + return (ExternalAssetOwnerLoanProductAttribute) implementingClass.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | ClassCastException exception) { + throw new IllegalStateException("Unable to create external asset owner loan product attribute: " + implementingClass.getName(), + exception); + } + } + private void validateLoanProduct(final Long loanProductId) { if (loanProductId == null) { throw new IllegalArgumentException("At least one of the following parameters must be provided: loanProductId"); @@ -70,7 +119,7 @@ private void validateLoanProduct(final Long loanProductId) { } } - public static Specification retrieveLoanProductAttributesByLoanProductIdAndAttributeKeySpecification( + private static Specification retrieveLoanProductAttributesByLoanProductIdAndAttributeKeySpecification( final Long loanProductId, final String attributeKey) { return (root, query, cb) -> { List predicates = new ArrayList<>(); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesWriteServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesWriteServiceImpl.java index 76109e156b6..3dee2ea0a5a 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesWriteServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesWriteServiceImpl.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -154,8 +153,7 @@ private void validateExternalAssetOwnerLoanProductAttribute(String attributeKey, if (implementingClass.isEnum()) { for (Object obj : implementingClass.getEnumConstants()) { ExternalAssetOwnerLoanProductAttribute objEnum = (ExternalAssetOwnerLoanProductAttribute) obj; - if (objEnum.getAttributeKey().equals(attributeKey) - && objEnum.getAttributeValue().equals(attributeValue.toUpperCase(Locale.ROOT))) { + if (objEnum.getAttributeKey().equals(attributeKey) && objEnum.validate(attributeValue)) { return; } } diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceTest.java index 81a3ba7b40e..275df80c025 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanProductAttributesReadServiceTest.java @@ -19,6 +19,7 @@ package org.apache.fineract.investor.service; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; @@ -31,6 +32,7 @@ import java.util.stream.Stream; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData; +import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesTemplateData; import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes; import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; @@ -101,6 +103,20 @@ public void testRetrieveAllLoanProductAttributesByLoanProductId(Long loanProduct verify(mapper, times(1)).mapLoanProductAttributes(attributes); } + @Test + public void testRetrieveExternalAssetOwnerLoanProductAttributesTemplate() { + // when + List result = underTest + .retrieveExternalAssetOwnerLoanProductAttributesTemplate(); + + // then + ExternalTransferLoanProductAttributesTemplateData settlementModel = result.stream() + .filter(attribute -> "SETTLEMENT_MODEL".equals(attribute.getAttributeKey())).findFirst().orElseThrow(); + assertEquals(1, result.size()); + assertEquals(List.of("DEFAULT_SETTLEMENT", "DELAYED_SETTLEMENT"), settlementModel.getAttributeValues()); + assertFalse(settlementModel.isMultiValue()); + } + @Test public void testRetrieveAllLoanProductAttributesByLoanProductIdAndInvalidAttributeKey() { // given