This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 955222ee82e5af7bb009fbf984f44855f97ea937 Author: Jose Alberto Hernandez <[email protected]> AuthorDate: Sat Feb 7 14:42:00 2026 -0500 FINERACT-2421: External Asset Owner create api --- .../commands/service/CommandWrapperBuilder.java | 7 ++ .../core/service/CommandParameterUtil.java | 1 + .../core/validator/ParseAndValidator.java | 17 +++++ .../AssetExternalizationStepDef.java | 78 ++++++++++++++++++++ .../fineract/test/support/TestContextKey.java | 2 + .../features/AssetExternalization.feature | 23 ++++++ .../api/ExternalAssetOwnersApiResource.java | 28 +++++++- .../api/ExternalAssetOwnersApiResourceSwagger.java | 18 +++++ .../investor/data/ExternalTransferOwnerData.java | 1 + .../ExternalAssetOwnerDuplicateException.java} | 20 +++--- .../serialization/ExternalAssetOwnerValidator.java | 51 ++++++++++++++ ...e.java => CreateExternalAssetOwnerHandler.java} | 21 +++--- .../service/ExternalAssetOwnersReadService.java | 5 ++ .../ExternalAssetOwnersReadServiceImpl.java | 9 +++ .../service/ExternalAssetOwnersTransferMapper.java | 3 + .../service/ExternalAssetOwnersWriteService.java | 1 + .../ExternalAssetOwnersWriteServiceImpl.java | 17 +++++ .../module/investor/module-changelog-master.xml | 1 + ..._add_external_asser_owner_create_permission.xml | 34 +++++++++ .../integrationtests/investor/InvestorHelper.java | 41 +++++++++++ .../externalassetowner/ExternalAssetOwnerTest.java | 82 ++++++++++++++++++++++ 21 files changed, 440 insertions(+), 20 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 135f937475..9a7302c033 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3732,6 +3732,13 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createExternalAssetOwner() { + this.actionName = "CREATE"; + this.entityName = "EXTERNAL_ASSET_OWNER"; + this.href = "/external-asset-owners"; + return this; + } + public CommandWrapperBuilder buybackLoanToExternalAssetOwner(final Long loanId) { this.actionName = "BUYBACK"; this.entityName = "LOAN"; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java index 7c0efb6524..f4e9a1dc44 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java @@ -30,6 +30,7 @@ public final class CommandParameterUtil { public static final String CANCEL_COMMAND_VALUE = "cancel"; public static final String UPDATE_COMMAND_VALUE = "update"; public static final String DELETE_COMMAND_VALUE = "delete"; + public static final String CREATE_COMMAND_VALUE = "create"; private CommandParameterUtil() {} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java index dbc6b285a0..c88384e36a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java @@ -18,8 +18,12 @@ */ package org.apache.fineract.infrastructure.core.validator; +import com.google.common.reflect.TypeToken; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -48,4 +52,17 @@ public class ParseAndValidator { dataValidator.getDataValidationErrors()); } } + + protected static void validateRequestBody(String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + } + + protected void validateForSupportedParameters(final String json, final Collection<String> supportedParameters, + final FromJsonHelper fromApiJsonHelper) { + final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters); + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java index 4f05df5207..2a93d76bc8 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java @@ -37,6 +37,7 @@ package org.apache.fineract.test.stepdef.assetexternalization; import static org.apache.fineract.client.feign.util.FeignCalls.fail; +import static org.apache.fineract.client.feign.util.FeignCalls.ok; import static org.assertj.core.api.Assertions.assertThat; import io.cucumber.datatable.DataTable; @@ -60,11 +61,14 @@ import org.apache.fineract.client.models.ExternalOwnerJournalEntryData; import org.apache.fineract.client.models.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.client.models.ExternalTransferData; import org.apache.fineract.client.models.ExternalTransferLoanProductAttributesData; +import org.apache.fineract.client.models.ExternalTransferOwnerData; import org.apache.fineract.client.models.GetLoanProductsResponse; import org.apache.fineract.client.models.JournalEntryData; import org.apache.fineract.client.models.PageExternalTransferData; import org.apache.fineract.client.models.PageExternalTransferLoanProductAttributesData; import org.apache.fineract.client.models.PostExternalAssetOwnerLoanProductAttributeRequest; +import org.apache.fineract.client.models.PostExternalAssetOwnerRequest; +import org.apache.fineract.client.models.PostExternalAssetOwnerResponse; import org.apache.fineract.client.models.PostInitiateTransferResponse; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PutExternalAssetOwnerLoanProductAttributeRequest; @@ -929,4 +933,78 @@ public class AssetExternalizationStepDef extends AbstractStepDef { createAssetExternalizationRequestByLoanId(table, transferExternalId, false); } + @When("Admin creates a new external asset owner with a unique ownerExternalId") + public void createExternalAssetOwnerWithUniqueId() { + String ownerExternalId = Utils.randomStringGenerator(OWNER_EXTERNAL_ID_PREFIX, 20); + testContext().set(TestContextKey.EXTERNAL_ASSET_OWNER_EXTERNAL_ID, ownerExternalId); + + PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(ownerExternalId); + PostExternalAssetOwnerResponse response = ok(() -> externalAssetOwnersApi().createExternalAssetOwner(request)); + testContext().set(TestContextKey.EXTERNAL_ASSET_OWNER_CREATE_RESPONSE, response); + + log.debug("Created external asset owner with externalId: {}, resourceId: {}", ownerExternalId, response.getResourceId()); + } + + @Then("External asset owner creation response has a non-null resourceId") + public void verifyCreateResponseHasResourceId() { + PostExternalAssetOwnerResponse response = testContext().get(TestContextKey.EXTERNAL_ASSET_OWNER_CREATE_RESPONSE); + assertThat(response).as("External asset owner create response should not be null").isNotNull(); + assertThat(response.getResourceId()).as("resourceId should not be null").isNotNull(); + } + + @Then("External asset owner list contains the created owner") + public void verifyOwnerExistsInList() { + String ownerExternalId = testContext().get(TestContextKey.EXTERNAL_ASSET_OWNER_EXTERNAL_ID); + PostExternalAssetOwnerResponse createResponse = testContext().get(TestContextKey.EXTERNAL_ASSET_OWNER_CREATE_RESPONSE); + + List<ExternalTransferOwnerData> owners = ok(() -> externalAssetOwnersApi().retrieveExternalAssetOwners()); + assertThat(owners).as("Owners list should not be empty").isNotEmpty(); + + ExternalTransferOwnerData found = owners.stream().filter(o -> ownerExternalId.equals(o.getExternalId())).findFirst().orElse(null); + + assertThat(found).as("Owner with externalId '%s' should exist in the list", ownerExternalId).isNotNull(); + assertThat(found.getId()).as("Owner id from GET should match resourceId from create").isEqualTo(createResponse.getResourceId()); + } + + @When("Admin tries to create an external asset owner with null ownerExternalId then it should fail with {int} status code") + public void createExternalAssetOwnerWithNullIdFails(int expectedStatusCode) { + PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(null); + + CallFailedRuntimeException exception = fail(() -> externalAssetOwnersApi().createExternalAssetOwner(request)); + + assertThat(exception.getStatus()).as("Expected HTTP %d for null ownerExternalId", expectedStatusCode).isEqualTo(expectedStatusCode); + assertThat(exception.getDeveloperMessage()).as("Error message should indicate ownerExternalId cannot be blank") + .containsAnyOf("validation.msg.externalAssetOwner.ownerExternalId.cannot.be.blank", "ownerExternalId"); + } + + @When("Admin tries to create an external asset owner with a duplicate ownerExternalId then it should fail with {int} status code") + public void createExternalAssetOwnerWithDuplicateIdFails(int expectedStatusCode) { + String ownerExternalId = testContext().get(TestContextKey.EXTERNAL_ASSET_OWNER_EXTERNAL_ID); + + PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(ownerExternalId); + + CallFailedRuntimeException exception = fail(() -> externalAssetOwnersApi().createExternalAssetOwner(request)); + + assertThat(exception.getStatus()).as("Expected HTTP %d for duplicate ownerExternalId", expectedStatusCode) + .isEqualTo(expectedStatusCode); + assertThat(exception.getDeveloperMessage()).as("Error message should indicate duplicate external id") + .contains("External Asset Owner id already exist"); + } + + @When("Admin tries to create an external asset owner with empty JSON body then it should fail with {int} status code") + public void createExternalAssetOwnerWithEmptyBodyFails(int expectedStatusCode) { + PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest(); + + CallFailedRuntimeException exception = fail(() -> externalAssetOwnersApi().createExternalAssetOwner(request)); + + assertThat(exception.getStatus()).as("Expected HTTP %d for missing ownerExternalId", expectedStatusCode) + .isEqualTo(expectedStatusCode); + } + + @Then("Admin retrieves all external asset owners successfully") + public void retrieveAllExternalAssetOwners() { + List<ExternalTransferOwnerData> owners = ok(() -> externalAssetOwnersApi().retrieveExternalAssetOwners()); + assertThat(owners).as("Owners list should not be null").isNotNull(); + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 99b5e28733..1b18697f58 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -255,6 +255,8 @@ public abstract class TestContextKey { public static final String ASSET_EXTERNALIZATION_BUYBACK_TRANSFER_PREFIX = "assetExternalizationTransferPrefix"; public static final String ASSET_EXTERNALIZATION_OWNER_EXTERNAL_ID = "assetExternalizationOwnerExternalId"; public static final String ASSET_EXTERNALIZATION_PREVIOUS_OWNER_EXTERNAL_ID = "assetExternalizationPreviousOwnerExternalId"; + public static final String EXTERNAL_ASSET_OWNER_CREATE_RESPONSE = "externalAssetOwnerCreateResponse"; + public static final String EXTERNAL_ASSET_OWNER_EXTERNAL_ID = "externalAssetOwnerExternalId"; public static final String TRANSACTION_EVENT = "transactionEvent"; public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse"; public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature index 0635bde96d..ddb17f7364 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature @@ -2019,3 +2019,26 @@ Feature: Asset Externalization Then LoanOwnershipTransferBusinessEvent with transfer type: "BUYBACK" and transfer asset owner based on intermediarySale is created When Admin set external asset owner loan product attribute "SETTLEMENT_MODEL" value "DEFAULT_SETTLEMENT" for loan product "LP1_DUE_DATE" + @TestRailId:C4640 + Scenario: Verify creation of new external asset owner and it presence in the list + When Admin creates a new external asset owner with a unique ownerExternalId + Then External asset owner creation response has a non-null resourceId + Then External asset owner list contains the created owner + + @TestRailId:C4641 + Scenario: Verify creation of an external asset owner fails for null, duplicate and empty ownerExternalId + When Admin tries to create an external asset owner with null ownerExternalId then it should fail with 400 status code + When Admin tries to create an external asset owner with empty JSON body then it should fail with 400 status code + When Admin creates a new external asset owner with a unique ownerExternalId + Then External asset owner creation response has a non-null resourceId + When Admin tries to create an external asset owner with a duplicate ownerExternalId then it should fail with 403 status code + + @TestRailId:C4642 + Scenario: Verify creation of multiple external asset owners and presence of all items the list + When Admin creates a new external asset owner with a unique ownerExternalId + Then External asset owner creation response has a non-null resourceId + Then External asset owner list contains the created owner + When Admin creates a new external asset owner with a unique ownerExternalId + Then External asset owner creation response has a non-null resourceId + Then External asset owner list contains the created owner + Then Admin retrieves all external asset owners successfully \ No newline at end of file diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java index f1a02f95ea..783b3c26db 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java @@ -20,6 +20,7 @@ package org.apache.fineract.investor.api; import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.BUY_BACK_COMMAND_VALUE; import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.CANCEL_COMMAND_VALUE; +import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.CREATE_COMMAND_VALUE; import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.INTERMEDIARY_SALE_COMMAND_VALUE; import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.SALE_COMMAND_VALUE; @@ -38,6 +39,7 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.batch.command.CommandHandlerRegistry; @@ -55,6 +57,7 @@ import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition; import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferOwnerData; import org.apache.fineract.investor.data.request.ExternalAssetOwnerRequest; import org.apache.fineract.investor.service.ExternalAssetOwnersReadService; import org.apache.fineract.investor.service.search.domain.ExternalAssetOwnerSearchRequest; @@ -85,7 +88,8 @@ public class ExternalAssetOwnersApiResource { (id, json) -> new CommandWrapperBuilder().withJson(json).intermediarySaleLoanToExternalAssetOwner(id).build(), SALE_COMMAND_VALUE, (id, json) -> new CommandWrapperBuilder().withJson(json).saleLoanToExternalAssetOwner(id).build(), BUY_BACK_COMMAND_VALUE, - (id, json) -> new CommandWrapperBuilder().withJson(json).buybackLoanToExternalAssetOwner(id).build())); + (id, json) -> new CommandWrapperBuilder().withJson(json).buybackLoanToExternalAssetOwner(id).build(), + CREATE_COMMAND_VALUE, (id, json) -> new CommandWrapperBuilder().withJson(json).createExternalAssetOwner().build())); @POST @Path("/transfers/loans/{loanId}") @@ -215,4 +219,26 @@ public class ExternalAssetOwnersApiResource { platformUserRightsContext.isAuthenticated(); return delegate.searchInvestorData(request); } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create an External Asset Owner using the External Id") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostExternalAssetOwnerRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostExternalAssetOwnerResponse.class))) + @ApiResponse(responseCode = "400", description = "Bad requests due invalid json data") + public CommandProcessingResult createExternalAssetOwner(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + platformUserRightsContext.isAuthenticated(); + final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(CREATE_COMMAND_VALUE, null, apiRequestBodyAsJson, + new UnrecognizedQueryParamException(COMMAND_PARAM, CREATE_COMMAND_VALUE)); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Get all External Asset Owner with details") + public List<ExternalTransferOwnerData> retrieveExternalAssetOwners() { + platformUserRightsContext.isAuthenticated(); + return externalAssetOwnersReadService.retrieveAllExternalOwners(); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java index 92e0b9a091..ea5a3f1830 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java @@ -68,4 +68,22 @@ final class ExternalAssetOwnersApiResourceSwagger { } } + @Schema(description = "PostExternalAssetOwnerRequest") + public static final class PostExternalAssetOwnerRequest { + + private PostExternalAssetOwnerRequest() {} + + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e", description = "External Asset Owner External Id") + public String ownerExternalId; + } + + @Schema(description = "PostExternalAssetOwnerResponse") + public static final class PostExternalAssetOwnerResponse { + + private PostExternalAssetOwnerResponse() {} + + @Schema(example = "1", description = "External Asset Owner Id") + public Long resourceId; + } + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferOwnerData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferOwnerData.java index edc1602bdd..c8e80a9d6f 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferOwnerData.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferOwnerData.java @@ -24,4 +24,5 @@ import lombok.Data; public class ExternalTransferOwnerData { private final String externalId; + private Long id; } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerDuplicateException.java similarity index 58% copy from fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java copy to fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerDuplicateException.java index 146a26421c..450a7f0f3f 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerDuplicateException.java @@ -16,19 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.investor.service; +package org.apache.fineract.investor.exception; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; -public interface ExternalAssetOwnersWriteService { - - CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand jsonCommand); - - CommandProcessingResult saleLoanByLoanId(JsonCommand command); - - CommandProcessingResult buybackLoanByLoanId(JsonCommand command); +/** + * A {@link RuntimeException} thrown when a GL Closure for a given date and Office combination is already present + */ +public class ExternalAssetOwnerDuplicateException extends AbstractPlatformDomainRuleException { - CommandProcessingResult cancelTransactionById(JsonCommand command); + public ExternalAssetOwnerDuplicateException(final String externalId) { + super("error.msg.provided.external.id.already.exists", "Provided external id already exists with Id " + externalId); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/serialization/ExternalAssetOwnerValidator.java b/fineract-investor/src/main/java/org/apache/fineract/investor/serialization/ExternalAssetOwnerValidator.java new file mode 100644 index 0000000000..918d335273 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/serialization/ExternalAssetOwnerValidator.java @@ -0,0 +1,51 @@ + +/** + * 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.serialization; + +import com.google.gson.JsonElement; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.validator.ParseAndValidator; +import org.apache.fineract.investor.data.ExternalTransferRequestParameters; +import org.apache.fineract.portfolio.common.service.Validator; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExternalAssetOwnerValidator extends ParseAndValidator { + + private final FromJsonHelper fromApiJsonHelper; + + public void validateForCreate(JsonCommand command) { + final String json = command.json(); + validateRequestBody(json); + validateForSupportedParameters(json, List.of(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID), fromApiJsonHelper); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + Validator.validateOrThrow("externalAssetOwner", baseDataValidator -> { + final String ownerExternalId = this.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, + element); + baseDataValidator.reset().parameter(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID).value(ownerExternalId).notNull(); + }); + } + +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CreateExternalAssetOwnerHandler.java similarity index 60% copy from fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java copy to fineract-investor/src/main/java/org/apache/fineract/investor/service/CreateExternalAssetOwnerHandler.java index 146a26421c..e06ddfa580 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CreateExternalAssetOwnerHandler.java @@ -18,17 +18,22 @@ */ package org.apache.fineract.investor.service; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.stereotype.Service; -public interface ExternalAssetOwnersWriteService { +@RequiredArgsConstructor +@Service +@CommandType(entity = "EXTERNAL_ASSET_OWNER", action = "CREATE") +public class CreateExternalAssetOwnerHandler implements NewCommandSourceHandler { - CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand jsonCommand); - - CommandProcessingResult saleLoanByLoanId(JsonCommand command); - - CommandProcessingResult buybackLoanByLoanId(JsonCommand command); - - CommandProcessingResult cancelTransactionById(JsonCommand command); + private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService; + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return externalAssetOwnersWriteService.createExternalAssetOwner(command); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java index 919551e0c8..d42338f975 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java @@ -18,10 +18,12 @@ */ package org.apache.fineract.investor.service; +import java.util.List; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferOwnerData; import org.springframework.data.domain.Page; public interface ExternalAssetOwnersReadService { @@ -40,4 +42,7 @@ public interface ExternalAssetOwnersReadService { ExternalTransferData retrieveFirstTransferByExternalId(ExternalId externalTransferId); Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId); + + List<ExternalTransferOwnerData> retrieveAllExternalOwners(); + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java index 8c86b932ae..8c336ab10b 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.investor.service; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.journalentry.JournalEntryMapper; @@ -26,8 +27,10 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferOwnerData; import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository; +import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository; @@ -52,6 +55,7 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository; private final ExternalAssetOwnersTransferMapper mapper; private final JournalEntryMapper journalEntryMapper; + private final ExternalAssetOwnerRepository externalAssetOwnerRepository; @Override public Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String externalTransferId, Integer offset, @@ -142,4 +146,9 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe return PageRequest.of(offset, limit, Sort.by("id")); } + @Override + public List<ExternalTransferOwnerData> retrieveAllExternalOwners() { + return mapper.mapOwners(externalAssetOwnerRepository.findAll()); + } + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java index b60e85e06d..3ca32ae3aa 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.investor.service; +import java.util.List; import org.apache.fineract.accounting.journalentry.JournalEntryMapper; import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; import org.apache.fineract.investor.data.ExternalTransferData; @@ -49,6 +50,8 @@ public interface ExternalAssetOwnersTransferMapper { ExternalTransferOwnerData mapOwner(ExternalAssetOwner source); + List<ExternalTransferOwnerData> mapOwners(List<ExternalAssetOwner> source); + @Mapping(target = "detailsId", source = "id") ExternalTransferDataDetails mapDetails(ExternalAssetOwnerTransferDetails details); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java index 146a26421c..8b0dea4cc9 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java @@ -31,4 +31,5 @@ public interface ExternalAssetOwnersWriteService { CommandProcessingResult cancelTransactionById(JsonCommand command); + CommandProcessingResult createExternalAssetOwner(JsonCommand command); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java index bb8477b239..8fd3873bd9 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java @@ -59,7 +59,9 @@ import org.apache.fineract.investor.domain.ExternalAssetOwner; import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.investor.exception.ExternalAssetOwnerDuplicateException; import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException; +import org.apache.fineract.investor.serialization.ExternalAssetOwnerValidator; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; @@ -82,6 +84,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW private final DelayedSettlementAttributeService delayedSettlementAttributeService; private final ConfigurationDomainService configurationDomainService; private final ExternalAssetOwnersReadService externalAssetOwnersReadService; + private final ExternalAssetOwnerValidator externalAssetOwnerValidator; @Override @Transactional @@ -601,4 +604,18 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW return configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer().stream() .map(LoanStatus::valueOf).collect(Collectors.toList()); } + + @Override + public CommandProcessingResult createExternalAssetOwner(JsonCommand command) { + externalAssetOwnerValidator.validateForCreate(command); + String ownerExternalId = command.stringValueOfParameterNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID); + Optional<ExternalAssetOwner> optExternalId = externalAssetOwnerRepository + .findByExternalId(ExternalIdFactory.produce(ownerExternalId)); + if (!optExternalId.isEmpty()) { + throw new ExternalAssetOwnerDuplicateException(ownerExternalId); + } + + final ExternalAssetOwner externalAssetOwner = createAndGetAssetOwner(ownerExternalId); + return new CommandProcessingResultBuilder().withEntityId(externalAssetOwner.getId()).build(); + } } diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index d7f6174a5e..10dc6bbf3f 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -43,4 +43,5 @@ <include relativeToChangelogFile="true" file="parts/0019_add_configurable_allowed_loan_statuses.xml"/> <include relativeToChangelogFile="true" file="parts/0020_add_previous_owner_reference.xml"/> <include relativeToChangelogFile="true" file="parts/0021_external_owner_reference_in_journal_entry_aggregation.xml"/> + <include relativeToChangelogFile="true" file="parts/0022_add_external_asser_owner_create_permission.xml"/> </databaseChangeLog> diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0022_add_external_asser_owner_create_permission.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0022_add_external_asser_owner_create_permission.xml new file mode 100644 index 0000000000..cf3438dae8 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0022_add_external_asser_owner_create_permission.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + <changeSet id="1" author="fineract"> + <insert tableName="m_permission"> + <column name="grouping" value="investor"/> + <column name="code" value="CREATE_EXTERNAL_ASSET_OWNER"/> + <column name="entity_name" value="EXTERNAL_ASSET_OWNER"/> + <column name="action_name" value="CREATE"/> + <column name="can_maker_checker" valueBoolean="false"/> + </insert> + </changeSet> +</databaseChangeLog> diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/InvestorHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/InvestorHelper.java new file mode 100644 index 0000000000..5aa7bec75b --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/InvestorHelper.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.integrationtests.investor; + +import java.util.List; +import org.apache.fineract.client.models.ExternalTransferOwnerData; +import org.apache.fineract.client.models.PostExternalAssetOwnerRequest; +import org.apache.fineract.client.models.PostExternalAssetOwnerResponse; +import org.apache.fineract.client.util.Calls; +import org.apache.fineract.integrationtests.common.FineractClientHelper; + +public final class InvestorHelper { + + private InvestorHelper() { + + } + + public static List<ExternalTransferOwnerData> retrieveExternalAssetOwners() { + return Calls.ok(FineractClientHelper.getFineractClient().externalAssetOwners.retrieveExternalAssetOwners()); + } + + public static PostExternalAssetOwnerResponse createExternalAssetOwner(final PostExternalAssetOwnerRequest request) { + return Calls.ok(FineractClientHelper.getFineractClient().externalAssetOwners.createExternalAssetOwner(request)); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTest.java new file mode 100644 index 0000000000..e9b091581f --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTest.java @@ -0,0 +1,82 @@ +/** + * 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.investor.externalassetowner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.ExternalTransferOwnerData; +import org.apache.fineract.client.models.PostExternalAssetOwnerRequest; +import org.apache.fineract.client.models.PostExternalAssetOwnerResponse; +import org.apache.fineract.client.util.CallFailedRuntimeException; +import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.investor.InvestorHelper; +import org.junit.jupiter.api.Test; + +@Slf4j +public class ExternalAssetOwnerTest extends BaseLoanIntegrationTest { + + @Test + public void testCreateExternalAssetOwnerSuccessfully() { + final String ownerExternalId = Utils.uniqueRandomStringGenerator("eao", 20); + final PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(ownerExternalId); + + final PostExternalAssetOwnerResponse response = InvestorHelper.createExternalAssetOwner(request); + + assertNotNull(response); + assertNotNull(response.getResourceId()); + + List<ExternalTransferOwnerData> externalAssetOwners = InvestorHelper.retrieveExternalAssetOwners(); + assertTrue(externalAssetOwners.size() > 0); + Optional<ExternalTransferOwnerData> optExternalTransferOwnerData = externalAssetOwners.stream() + .filter(eao -> eao.getExternalId().equals(ownerExternalId)).findFirst(); + assertTrue(optExternalTransferOwnerData.isPresent()); + } + + @Test + public void testCreateExternalAssetOwnerFailsWhenOwnerExternalIdIsNull() { + final PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(null); + + final CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> InvestorHelper.createExternalAssetOwner(request)); + assertEquals(400, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("validation.msg.externalAssetOwner.ownerExternalId.cannot.be.blank")); + } + + @Test + public void testCreateExternalAssetOwnerFailsWhenOwnerExternalIdIsDuplicate() { + final String ownerExternalId = Utils.uniqueRandomStringGenerator("eao", 20); + final PostExternalAssetOwnerRequest request = new PostExternalAssetOwnerRequest().ownerExternalId(ownerExternalId); + + final PostExternalAssetOwnerResponse firstResponse = InvestorHelper.createExternalAssetOwner(request); + assertNotNull(firstResponse); + assertNotNull(firstResponse.getResourceId()); + + final CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> InvestorHelper.createExternalAssetOwner(request)); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.provided.external.id.already.exists")); + } +}
