FINERACT-2421: External Asset Owner create api

This commit is contained in:
Jose Alberto Hernandez
2026-02-12 10:58:24 +01:00
committed by Adam Saghy
parent 6875645825
commit 955222ee82
21 changed files with 489 additions and 1 deletions
@@ -3732,6 +3732,13 @@ public class CommandWrapperBuilder {
return this; 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) { public CommandWrapperBuilder buybackLoanToExternalAssetOwner(final Long loanId) {
this.actionName = "BUYBACK"; this.actionName = "BUYBACK";
this.entityName = "LOAN"; this.entityName = "LOAN";
@@ -30,6 +30,7 @@ public final class CommandParameterUtil {
public static final String CANCEL_COMMAND_VALUE = "cancel"; public static final String CANCEL_COMMAND_VALUE = "cancel";
public static final String UPDATE_COMMAND_VALUE = "update"; public static final String UPDATE_COMMAND_VALUE = "update";
public static final String DELETE_COMMAND_VALUE = "delete"; public static final String DELETE_COMMAND_VALUE = "delete";
public static final String CREATE_COMMAND_VALUE = "create";
private CommandParameterUtil() {} private CommandParameterUtil() {}
@@ -18,8 +18,12 @@
*/ */
package org.apache.fineract.infrastructure.core.validator; package org.apache.fineract.infrastructure.core.validator;
import com.google.common.reflect.TypeToken;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -48,4 +52,17 @@ public class ParseAndValidator {
dataValidator.getDataValidationErrors()); 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);
}
} }
@@ -37,6 +37,7 @@
package org.apache.fineract.test.stepdef.assetexternalization; 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.fail;
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import io.cucumber.datatable.DataTable; 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.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.client.models.ExternalTransferData; import org.apache.fineract.client.models.ExternalTransferData;
import org.apache.fineract.client.models.ExternalTransferLoanProductAttributesData; 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.GetLoanProductsResponse;
import org.apache.fineract.client.models.JournalEntryData; import org.apache.fineract.client.models.JournalEntryData;
import org.apache.fineract.client.models.PageExternalTransferData; import org.apache.fineract.client.models.PageExternalTransferData;
import org.apache.fineract.client.models.PageExternalTransferLoanProductAttributesData; import org.apache.fineract.client.models.PageExternalTransferLoanProductAttributesData;
import org.apache.fineract.client.models.PostExternalAssetOwnerLoanProductAttributeRequest; 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.PostInitiateTransferResponse;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PutExternalAssetOwnerLoanProductAttributeRequest; import org.apache.fineract.client.models.PutExternalAssetOwnerLoanProductAttributeRequest;
@@ -929,4 +933,78 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
createAssetExternalizationRequestByLoanId(table, transferExternalId, false); 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();
}
} }
@@ -255,6 +255,8 @@ public abstract class TestContextKey {
public static final String ASSET_EXTERNALIZATION_BUYBACK_TRANSFER_PREFIX = "assetExternalizationTransferPrefix"; 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_OWNER_EXTERNAL_ID = "assetExternalizationOwnerExternalId";
public static final String ASSET_EXTERNALIZATION_PREVIOUS_OWNER_EXTERNAL_ID = "assetExternalizationPreviousOwnerExternalId"; 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 TRANSACTION_EVENT = "transactionEvent";
public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse"; public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse";
public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse"; public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse";
@@ -2019,3 +2019,26 @@ Feature: Asset Externalization
Then LoanOwnershipTransferBusinessEvent with transfer type: "BUYBACK" and transfer asset owner based on intermediarySale is created 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" 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
@@ -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.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.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.INTERMEDIARY_SALE_COMMAND_VALUE;
import static org.apache.fineract.infrastructure.core.service.CommandParameterUtil.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.Produces;
import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry; 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.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData; 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.data.request.ExternalAssetOwnerRequest;
import org.apache.fineract.investor.service.ExternalAssetOwnersReadService; import org.apache.fineract.investor.service.ExternalAssetOwnersReadService;
import org.apache.fineract.investor.service.search.domain.ExternalAssetOwnerSearchRequest; 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(), (id, json) -> new CommandWrapperBuilder().withJson(json).intermediarySaleLoanToExternalAssetOwner(id).build(),
SALE_COMMAND_VALUE, (id, json) -> new CommandWrapperBuilder().withJson(json).saleLoanToExternalAssetOwner(id).build(), SALE_COMMAND_VALUE, (id, json) -> new CommandWrapperBuilder().withJson(json).saleLoanToExternalAssetOwner(id).build(),
BUY_BACK_COMMAND_VALUE, 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 @POST
@Path("/transfers/loans/{loanId}") @Path("/transfers/loans/{loanId}")
@@ -215,4 +219,26 @@ public class ExternalAssetOwnersApiResource {
platformUserRightsContext.isAuthenticated(); platformUserRightsContext.isAuthenticated();
return delegate.searchInvestorData(request); 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();
}
} }
@@ -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;
}
} }
@@ -24,4 +24,5 @@ import lombok.Data;
public class ExternalTransferOwnerData { public class ExternalTransferOwnerData {
private final String externalId; private final String externalId;
private Long id;
} }
@@ -0,0 +1,32 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Closure for a given date and Office combination is already present
*/
public class ExternalAssetOwnerDuplicateException extends AbstractPlatformDomainRuleException {
public ExternalAssetOwnerDuplicateException(final String externalId) {
super("error.msg.provided.external.id.already.exists", "Provided external id already exists with Id " + externalId);
}
}
@@ -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();
});
}
}
@@ -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.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;
@RequiredArgsConstructor
@Service
@CommandType(entity = "EXTERNAL_ASSET_OWNER", action = "CREATE")
public class CreateExternalAssetOwnerHandler implements NewCommandSourceHandler {
private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnersWriteService.createExternalAssetOwner(command);
}
}
@@ -18,10 +18,12 @@
*/ */
package org.apache.fineract.investor.service; package org.apache.fineract.investor.service;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData; import org.apache.fineract.investor.data.ExternalTransferData;
import org.apache.fineract.investor.data.ExternalTransferOwnerData;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
public interface ExternalAssetOwnersReadService { public interface ExternalAssetOwnersReadService {
@@ -40,4 +42,7 @@ public interface ExternalAssetOwnersReadService {
ExternalTransferData retrieveFirstTransferByExternalId(ExternalId externalTransferId); ExternalTransferData retrieveFirstTransferByExternalId(ExternalId externalTransferId);
Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId); Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId);
List<ExternalTransferOwnerData> retrieveAllExternalOwners();
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.investor.service; package org.apache.fineract.investor.service;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.JournalEntryMapper; 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.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData; import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData; 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.ExternalAssetOwnerJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository; 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.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
@@ -52,6 +55,7 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository; private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
private final ExternalAssetOwnersTransferMapper mapper; private final ExternalAssetOwnersTransferMapper mapper;
private final JournalEntryMapper journalEntryMapper; private final JournalEntryMapper journalEntryMapper;
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
@Override @Override
public Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String externalTransferId, Integer offset, 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")); return PageRequest.of(offset, limit, Sort.by("id"));
} }
@Override
public List<ExternalTransferOwnerData> retrieveAllExternalOwners() {
return mapper.mapOwners(externalAssetOwnerRepository.findAll());
}
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.investor.service; package org.apache.fineract.investor.service;
import java.util.List;
import org.apache.fineract.accounting.journalentry.JournalEntryMapper; import org.apache.fineract.accounting.journalentry.JournalEntryMapper;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.investor.data.ExternalTransferData; import org.apache.fineract.investor.data.ExternalTransferData;
@@ -49,6 +50,8 @@ public interface ExternalAssetOwnersTransferMapper {
ExternalTransferOwnerData mapOwner(ExternalAssetOwner source); ExternalTransferOwnerData mapOwner(ExternalAssetOwner source);
List<ExternalTransferOwnerData> mapOwners(List<ExternalAssetOwner> source);
@Mapping(target = "detailsId", source = "id") @Mapping(target = "detailsId", source = "id")
ExternalTransferDataDetails mapDetails(ExternalAssetOwnerTransferDetails details); ExternalTransferDataDetails mapDetails(ExternalAssetOwnerTransferDetails details);
@@ -31,4 +31,5 @@ public interface ExternalAssetOwnersWriteService {
CommandProcessingResult cancelTransactionById(JsonCommand command); CommandProcessingResult cancelTransactionById(JsonCommand command);
CommandProcessingResult createExternalAssetOwner(JsonCommand command);
} }
@@ -59,7 +59,9 @@ import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; 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.exception.ExternalAssetOwnerInitiateTransferException;
import org.apache.fineract.investor.serialization.ExternalAssetOwnerValidator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -82,6 +84,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
private final DelayedSettlementAttributeService delayedSettlementAttributeService; private final DelayedSettlementAttributeService delayedSettlementAttributeService;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final ExternalAssetOwnersReadService externalAssetOwnersReadService; private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
private final ExternalAssetOwnerValidator externalAssetOwnerValidator;
@Override @Override
@Transactional @Transactional
@@ -601,4 +604,18 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
return configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer().stream() return configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer().stream()
.map(LoanStatus::valueOf).collect(Collectors.toList()); .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();
}
} }
@@ -43,4 +43,5 @@
<include relativeToChangelogFile="true" file="parts/0019_add_configurable_allowed_loan_statuses.xml"/> <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/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/0021_external_owner_reference_in_journal_entry_aggregation.xml"/>
<include relativeToChangelogFile="true" file="parts/0022_add_external_asser_owner_create_permission.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -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>
@@ -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));
}
}
@@ -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"));
}
}