From 045f0ec121a0e5a5ed6a4335f50f6286ccc4a36b Mon Sep 17 00:00:00 2001 From: taskain7 Date: Tue, 23 May 2023 07:00:58 +0200 Subject: [PATCH] [FINERACT-1926] POST API for asset externalization --- .../service/CommandWrapperBuilder.java | 16 + ...olioCommandSourceWritePlatformService.java | 0 .../cache/domain/CacheType.java | 0 .../domain/ConfigurationDomainService.java | 0 .../data/CommandProcessingResultBuilder.java | 0 .../core/data/LoanIdAndExternalIdData.java | 31 ++ .../UnrecognizedQueryParamException.java | 0 .../core/service/CommandParameterUtil.java | 0 .../core/service/ExternalIdFactory.java | 0 .../LoanReadPlatformServiceCommon.java | 28 ++ .../api/ExternalAssetOwnersApiResource.java | 46 ++- ...ExternalAssetOwnersApiResourceSwagger.java | 69 +++- .../data/ExternalTransferChangedData.java | 31 ++ .../investor/data/ExternalTransferData.java | 3 +- .../ExternalTransferRequestParameters.java | 31 ++ .../data/ExternalTransferResponseData.java | 33 ++ .../investor/domain/ExternalAssetOwner.java | 3 +- .../domain/ExternalAssetOwnerRepository.java | 4 + .../domain/ExternalAssetOwnerTransfer.java | 8 +- .../ExternalAssetOwnerTransferRepository.java | 13 +- .../investor/domain/ExternalIdConverter.java | 40 +++ ...alAssetOwnerInitiateTransferException.java | 26 ++ ...tOwnerInitiateTransferExceptionMapper.java | 45 +++ ...backLoanFromExternalAssetOwnerHandler.java | 39 +++ .../ExternalAssetOwnersReadServiceImpl.java | 13 +- .../ExternalAssetOwnersWriteService.java | 29 ++ .../ExternalAssetOwnersWriteServiceImpl.java | 215 +++++++++++++ .../SaleLoanToExternalAssetOwnerHandler.java | 39 +++ .../investor/module-changelog-master.xml | 3 + .../investor/parts/0003_asset_schemas.xml | 20 ++ .../0004_change_purchase_price_ratio_type.xml | 36 +++ .../0005_add_sale_and_buyback_command.xml | 41 +++ .../cob/data/LoanIdAndExternalId.java | 28 ++ .../core/auditing/CustomAuditingHandler.java | 7 +- .../core/domain/ExternalIdConverter.java | 3 +- .../loanaccount/domain/LoanRepository.java | 11 + .../domain/LoanRepositoryWrapper.java | 4 + .../service/LoanReadPlatformServiceImpl.java | 14 +- .../ExternalAssetOwnerHelper.java | 53 ++++ ...nitiateExternalAssetOwnerTransferTest.java | 300 ++++++++++++++++++ 40 files changed, 1257 insertions(+), 25 deletions(-) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformService.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/cache/domain/CacheType.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java (100%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/core/exception/UnrecognizedQueryParamException.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java (100%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferChangedData.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalIdConverter.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerInitiateTransferException.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/exception/exceptionmapper/ExternalAssetOwnerInitiateTransferExceptionMapper.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/SaleLoanToExternalAssetOwnerHandler.java create mode 100644 fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0004_change_purchase_price_ratio_type.xml create mode 100644 fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0005_add_sale_and_buyback_command.xml create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java 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 1ffc5265e..ca182beff 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 @@ -3622,4 +3622,20 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId + "/transactions?command=undo-charge-off"; return this; } + + public CommandWrapperBuilder saleLoanToExternalAssetOwner(final Long loanId) { + this.actionName = "SALE"; + this.entityName = "LOAN"; + this.loanId = loanId; + this.href = "/external-asset-owners/transfers/loans/" + loanId; + return this; + } + + public CommandWrapperBuilder buybackLoanToExternalAssetOwner(final Long loanId) { + this.actionName = "BUYBACK"; + this.entityName = "LOAN"; + this.loanId = loanId; + this.href = "/external-asset-owners/transfers/loans/" + loanId; + return this; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformService.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformService.java rename to fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformService.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/domain/CacheType.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/domain/CacheType.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/cache/domain/CacheType.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/domain/CacheType.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java new file mode 100644 index 000000000..f09191a3a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java @@ -0,0 +1,31 @@ +/** + * 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.infrastructure.core.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +@Data +@AllArgsConstructor +public class LoanIdAndExternalIdData { + + private Long loanId; + private ExternalId loanExternalId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/UnrecognizedQueryParamException.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/UnrecognizedQueryParamException.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/UnrecognizedQueryParamException.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/UnrecognizedQueryParamException.java diff --git a/fineract-provider/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 similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/CommandParameterUtil.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java new file mode 100644 index 000000000..871be9ab2 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; + +public interface LoanReadPlatformServiceCommon { + + LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId); + + Long getLoanIdByLoanExternalId(String externalId); +} 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 6c9f55bac..c918e7dee 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 @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -37,13 +38,20 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.NotImplementedException; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.security.service.PlatformUserRightsContext; import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferResponseData; import org.apache.fineract.investor.service.ExternalAssetOwnersReadService; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon; import org.springframework.stereotype.Component; @Path("/v1/external-asset-owners") @@ -56,29 +64,40 @@ public class ExternalAssetOwnersApiResource { private final ExternalAssetOwnersReadService externalAssetOwnersReadService; private final ApiRequestParameterHelper apiRequestParameterHelper; private final DefaultToApiJsonSerializer apiJsonSerializerService; + private final DefaultToApiJsonSerializer postApiJsonSerializerService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final LoanReadPlatformServiceCommon loanReadPlatformService; @POST @Path("/transfers/loans/{loanId}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferResponse.class))), + @ApiResponse(responseCode = "403", description = "Transfer cannot be initiated") }) public String transferRequestWithLoanId(@PathParam("loanId") final Long loanId, @QueryParam("command") @Parameter(description = "command") final String commandParam, @Parameter(hidden = true) final String apiRequestBodyAsJson) { platformUserRightsContext.isAuthenticated(); - - throw new NotImplementedException("Not implemented yet"); + return getResult(loanId, apiRequestBodyAsJson, commandParam); } @POST @Path("/transfers/loans/external-id/{loanExternalId}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String transferRequestWithLoanExternalId(@PathParam("loanExternalId") final Long loanId, + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferResponse.class))), + @ApiResponse(responseCode = "403", description = "Transfer cannot be initiated") }) + public String transferRequestWithLoanExternalId(@PathParam("loanExternalId") final String externalLoanId, @QueryParam("command") @Parameter(description = "command") final String commandParam, @Parameter(hidden = true) final String apiRequestBodyAsJson) { platformUserRightsContext.isAuthenticated(); + Long loanId = loanReadPlatformService.getLoanIdByLoanExternalId(externalLoanId); - throw new NotImplementedException("Not implemented yet"); + return getResult(loanId, apiRequestBodyAsJson, commandParam); } @GET @@ -99,4 +118,21 @@ public class ExternalAssetOwnersApiResource { ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return apiJsonSerializerService.serialize(settings, transferDataList); } + + private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) { + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + CommandWrapper commandRequest = null; + if (CommandParameterUtil.is(commandParam, "sale")) { + commandRequest = builder.saleLoanToExternalAssetOwner(loanId).build(); + } else if (CommandParameterUtil.is(commandParam, "buyback")) { + commandRequest = builder.buybackLoanToExternalAssetOwner(loanId).build(); + } + + if (commandRequest == null) { + throw new UnrecognizedQueryParamException("command", commandParam); + } + CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return postApiJsonSerializerService.serialize(result); + } } 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 43976241e..aba5bd722 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 @@ -19,7 +19,6 @@ package org.apache.fineract.investor.api; import io.swagger.v3.oas.annotations.media.Schema; -import java.math.BigDecimal; import java.time.LocalDate; import java.util.Set; import org.apache.fineract.investor.data.ExternalTransferStatus; @@ -68,7 +67,7 @@ final class ExternalAssetOwnersApiResourceSwagger { public String transferExternalId; @Schema(example = "1") - public BigDecimal purchasePriceRatio; + public String purchasePriceRatio; @Schema(example = "[2023, 5, 23]") public LocalDate settlementDate; @@ -87,4 +86,70 @@ final class ExternalAssetOwnersApiResourceSwagger { public Integer totalFilteredRecords; public Set pageItems; } + + @Schema(description = "PostInitiateTransferRequest") + public static final class PostInitiateTransferRequest { + + private PostInitiateTransferRequest() {} + + @Schema(example = "[2023, 5, 23]") + public LocalDate settlementDate; + + @Schema(example = "1234567890987654321abc") + public String ownerExternalId; + + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e") + public String transferExternalId; + + @Schema(example = "1.2345678") + public String purchasePriceRatio; + + @Schema(example = "yyyy-MM-dd") + public String dateformat; + + @Schema(example = "en") + public String locale; + } + + @Schema(description = "PostInitiateTransferResponse") + public static final class PostInitiateTransferResponse { + + private PostInitiateTransferResponse() {} + + @Schema(example = "1", description = "transfer ID") + public Long resourceId; + + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e", description = "transfer external ID") + public String resourceExternalId; + + @Schema(example = "2", description = "loan ID") + public Long subResourceId; + + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c2e", description = "loan external ID") + public String subResourceExternalId; + + public ExternalAssetOwnerTransferChangesData changes; + + @Schema(example = "yyyy-MM-dd") + public String dateformat; + + @Schema(example = "en") + public String locale; + + @Schema(description = "ExternalAssetOwnerTransferChangesData") + static final class ExternalAssetOwnerTransferChangesData { + + @Schema(example = "[2023, 5, 23]") + public String settlementDate; + + @Schema(example = "1234567890987654321abc") + public String ownerExternalId; + + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e") + public String transferExternalId; + + @Schema(example = "1.23456789") + public String purchasePriceRatio; + } + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferChangedData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferChangedData.java new file mode 100644 index 000000000..7df77203b --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferChangedData.java @@ -0,0 +1,31 @@ +/** + * 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.time.LocalDate; +import lombok.Data; + +@Data +public class ExternalTransferChangedData { + + private LocalDate settlementDate; + private String transferExternalId; + private String ownerExternalId; + private String purchasePriceRatio; +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java index 23f2bb861..ca1d1d878 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java @@ -18,7 +18,6 @@ */ package org.apache.fineract.investor.data; -import java.math.BigDecimal; import java.time.LocalDate; import lombok.Data; @@ -29,7 +28,7 @@ public class ExternalTransferData { private ExternalTransferOwnerData owner; private ExternalTransferLoanMappingData loan; private String transferExternalId; - private BigDecimal purchasePriceRatio; + private String purchasePriceRatio; private LocalDate settlementDate; private ExternalTransferStatus status; private LocalDate effectiveFrom; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java new file mode 100644 index 000000000..127b58727 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java @@ -0,0 +1,31 @@ +/** + * 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; + +public final class ExternalTransferRequestParameters { + + private ExternalTransferRequestParameters() {} + + public static final String SETTLEMENT_DATE = "settlement_date"; + public static final String OWNER_EXTERNAL_ID = "owner_external_id"; + public static final String TRANSFER_EXTERNAL_ID = "transfer_external_id"; + public static final String PURCHASE_PRICE_RATIO = "purchase_price_ratio"; + public static final String DATEFORMAT = "dateformat"; + public static final String LOCALE = "locale"; +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java new file mode 100644 index 000000000..b6cc3bbd5 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java @@ -0,0 +1,33 @@ +/** + * 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 lombok.Data; + +@Data +public class ExternalTransferResponseData { + + private Long resourceId; + private String resourceExternalId; + private Long subResourceId; + private String subResourceExternalId; + private ExternalTransferChangedData changes; + private String dateformat; + private String locale; +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwner.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwner.java index fd7c4d3de..4f2422c78 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwner.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwner.java @@ -21,7 +21,6 @@ package org.apache.fineract.investor.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -30,7 +29,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; @Getter @Setter @Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @Table(name = "m_external_asset_owner") public class ExternalAssetOwner extends AbstractAuditableWithUTCDateTimeCustom { diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerRepository.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerRepository.java index eee4e5d8e..90e554247 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerRepository.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerRepository.java @@ -18,10 +18,14 @@ */ package org.apache.fineract.investor.domain; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface ExternalAssetOwnerRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByExternalId(ExternalId externalId); + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java index e292ba6c0..468c3dc22 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java @@ -18,14 +18,12 @@ */ package org.apache.fineract.investor.domain; -import java.math.BigDecimal; import java.time.LocalDate; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -34,7 +32,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; @Getter @Setter @Table(name = "m_external_asset_owner_transfer") -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @Entity public class ExternalAssetOwnerTransfer extends AbstractAuditableWithUTCDateTimeCustom { @@ -51,8 +49,8 @@ public class ExternalAssetOwnerTransfer extends AbstractAuditableWithUTCDateTime @Column(name = "status", length = 50) private String status; - @Column(name = "purchase_price_ratio", precision = 19, scale = 6) - private BigDecimal purchasePriceRatio; + @Column(name = "purchase_price_ratio", length = 50) + private String purchasePriceRatio; @Column(name = "settlement_date") private LocalDate settlementDate; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java index 556c33fe6..799143d0a 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java @@ -19,6 +19,8 @@ package org.apache.fineract.investor.domain; import java.util.List; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; @@ -27,7 +29,12 @@ import org.springframework.data.repository.query.Param; public interface ExternalAssetOwnerTransferRepository extends JpaRepository, JpaSpecificationExecutor { - @Query("select e from ExternalAssetOwnerTransfer e where (:loanId is null or e.loanId = :loanId) and (:loanExternalId is null or e.externalLoanId = :loanExternalId) and (:transferExternalId is null or e.externalId = :transferExternalId)") - List findAllByIncomingId(@Param("loanId") Long loanId, @Param("loanExternalId") String loanExternalId, - @Param("transferExternalId") String transferExternalId); + List findAllByLoanId(Long loanId); + + List findAllByExternalLoanId(ExternalId externalLoanId); + + List findAllByExternalId(ExternalId externalId); + + @Query("select e from ExternalAssetOwnerTransfer e where e.loanId = :loanId and e.id = (select max(ex.id) from ExternalAssetOwnerTransfer ex where ex.loanId = :loanId)") + Optional findLatestByLoanId(@Param("loanId") Long loanId); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalIdConverter.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalIdConverter.java new file mode 100644 index 000000000..b51da860a --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalIdConverter.java @@ -0,0 +1,40 @@ +/** + * 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.domain; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; + +@Converter(autoApply = true) +public class ExternalIdConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ExternalId externalId) { + return externalId != null ? externalId.getValue() : null; + } + + @Override + public ExternalId convertToEntityAttribute(String externalId) { + return StringUtils.isBlank(externalId) ? ExternalId.empty() : ExternalIdFactory.produce(externalId); + } + +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerInitiateTransferException.java b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerInitiateTransferException.java new file mode 100644 index 000000000..6fc4bcd8a --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerInitiateTransferException.java @@ -0,0 +1,26 @@ +/** + * 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; + +public class ExternalAssetOwnerInitiateTransferException extends RuntimeException { + + public ExternalAssetOwnerInitiateTransferException(String message) { + super(message); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/exceptionmapper/ExternalAssetOwnerInitiateTransferExceptionMapper.java b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/exceptionmapper/ExternalAssetOwnerInitiateTransferExceptionMapper.java new file mode 100644 index 000000000..bdae122d0 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/exceptionmapper/ExternalAssetOwnerInitiateTransferExceptionMapper.java @@ -0,0 +1,45 @@ +/** + * 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.exceptionmapper; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException; +import org.springframework.stereotype.Component; + +@Provider +@Component +@Slf4j +public class ExternalAssetOwnerInitiateTransferExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ExternalAssetOwnerInitiateTransferException exception) { + final String globalisationMessageCode = "error.msg.external.asset.owner.initiate"; + final String defaultUserMessage = exception.getMessage(); + log.warn("Exception: {}, Message: {}", exception.getClass().getName(), defaultUserMessage); + + final ApiParameterError error = ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage); + + return Response.status(Response.Status.FORBIDDEN).entity(error).type(MediaType.APPLICATION_JSON).build(); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java new file mode 100644 index 000000000..9fc11ca38 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.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.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 = "LOAN", action = "BUYBACK") +public class BuybackLoanFromExternalAssetOwnerHandler implements NewCommandSourceHandler { + + private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return externalAssetOwnersWriteService.buyBackLoanByLoanId(command); + } +} 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 b5b61db38..392fa9b24 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,8 +18,10 @@ */ package org.apache.fineract.investor.service; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.investor.data.ExternalTransferData; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; @@ -33,12 +35,17 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; private final ExternalAssetOwnersTransferMapper mapper; - private final ExternalAssetOwnersTransferMapper externalAssetOwnersTransferMapper; @Override public List retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId) { - List result = externalAssetOwnerTransferRepository.findAllByIncomingId(loanId, externalLoanId, - transferExternalId); + List result = new ArrayList<>(); + if (loanId != null) { + result.addAll(externalAssetOwnerTransferRepository.findAllByLoanId(loanId)); + } else if (externalLoanId != null) { + result.addAll(externalAssetOwnerTransferRepository.findAllByExternalLoanId(ExternalIdFactory.produce(externalLoanId))); + } else if (transferExternalId != null) { + result.addAll(externalAssetOwnerTransferRepository.findAllByExternalId(ExternalIdFactory.produce(transferExternalId))); + } return result.stream().map(mapper::mapTransfer).toList(); } } 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 new file mode 100644 index 000000000..b3133875d --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java @@ -0,0 +1,29 @@ +/** + * 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 org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface ExternalAssetOwnersWriteService { + + CommandProcessingResult saleLoanByLoanId(JsonCommand command); + + CommandProcessingResult buyBackLoanByLoanId(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 new file mode 100644 index 000000000..50876c654 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java @@ -0,0 +1,215 @@ +/** + * 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 com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.investor.data.ExternalTransferRequestParameters; +import org.apache.fineract.investor.data.ExternalTransferStatus; +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.ExternalAssetOwnerInitiateTransferException; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService { + + private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; + private final ExternalAssetOwnerRepository externalAssetOwnerRepository; + private final FromJsonHelper fromApiJsonHelper; + private final LoanReadPlatformServiceCommon loanReadPlatformService; + + @Override + @Transactional + public CommandProcessingResult saleLoanByLoanId(JsonCommand command) { + Long loanId = command.getLoanId(); + LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId); + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(loanId, command.json(), loanIdAndExternalId.getLoanExternalId(), + ExternalTransferStatus.PENDING); + validateSale(externalAssetOwnerTransfer, loanIdAndExternalId); + ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); + return buildResponseData(savedExternalAssetOwnerTransfer); + } + + @Override + @Transactional + public CommandProcessingResult buyBackLoanByLoanId(JsonCommand command) { + Long loanId = command.getLoanId(); + LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId); + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(command.getLoanId(), command.json(), + loanIdAndExternalId.getLoanExternalId(), ExternalTransferStatus.BUYBACK); + validateBuyBack(externalAssetOwnerTransfer); + ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); + return buildResponseData(savedExternalAssetOwnerTransfer); + } + + private CommandProcessingResult buildResponseData(ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer) { + Map changes = new HashMap<>(); + changes.put(ExternalTransferRequestParameters.SETTLEMENT_DATE, savedExternalAssetOwnerTransfer.getSettlementDate()); + changes.put(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, + savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue()); + changes.put(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, + savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue()); + changes.put(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, savedExternalAssetOwnerTransfer.getPurchasePriceRatio()); + return new CommandProcessingResultBuilder().withEntityId(savedExternalAssetOwnerTransfer.getId()) + .withEntityExternalId(savedExternalAssetOwnerTransfer.getExternalId()) + .withSubEntityId(savedExternalAssetOwnerTransfer.getLoanId()) + .withSubEntityExternalId(Objects.isNull(savedExternalAssetOwnerTransfer.getExternalLoanId()) ? null + : savedExternalAssetOwnerTransfer.getExternalLoanId()) + .with(changes).build(); + } + + private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer, LoanIdAndExternalIdData loanIdAndExternalId) { + validateSettlementDate(externalAssetOwnerTransfer); + validateTransferStatusForSale(externalAssetOwnerTransfer); + validateLoanStatus(loanIdAndExternalId); + } + + private void validateBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + validateSettlementDate(externalAssetOwnerTransfer); + validateTransferStatusForBuyBack(externalAssetOwnerTransfer); + } + + private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + if (externalAssetOwnerTransfer.getSettlementDate().isBefore(ThreadLocalContextUtil.getBusinessDate())) { + throw new ExternalAssetOwnerInitiateTransferException("Settlement date cannot be in the past"); + } + } + + private void validateLoanStatus(LoanIdAndExternalIdData loanIdAndExternalIdAndExternalId) { + if (Objects.isNull(loanIdAndExternalIdAndExternalId)) { + throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status"); + } + } + + private void validateTransferStatusForSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + Optional latestTransferOptional = externalAssetOwnerTransferRepository + .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId()); + if (latestTransferOptional.isPresent()) { + ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get(); + ExternalTransferStatus latestTransferStatus = ExternalTransferStatus.valueOf(latestTransfer.getStatus()); + if (latestTransferStatus.equals(ExternalTransferStatus.PENDING)) { + throw new ExternalAssetOwnerInitiateTransferException( + "External asset owner transfer is already in PENDING state for this loan."); + } else if (latestTransferStatus.equals(ExternalTransferStatus.ACTIVE)) { + throw new ExternalAssetOwnerInitiateTransferException( + "This loan cannot be sold, because it is owned by an external asset owner."); + } + } + } + + private void validateTransferStatusForBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + Optional latestTransferOptional = externalAssetOwnerTransferRepository + .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId()); + if (latestTransferOptional.isEmpty()) { + throw new ExternalAssetOwnerInitiateTransferException( + "This loan cannot be bought back, because it is not owned by an external asset owner"); + } else { + ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get(); + ExternalTransferStatus latestTransferStatus = ExternalTransferStatus.valueOf(latestTransfer.getStatus()); + if (latestTransferStatus.equals(ExternalTransferStatus.BUYBACK)) { + throw new ExternalAssetOwnerInitiateTransferException( + "External asset owner transfer is already in BUYBACK state for this loan."); + } + } + } + + private ExternalAssetOwnerTransfer parseJson(Long loanId, String apiRequestBodyAsJson, ExternalId externalLoanId, + ExternalTransferStatus status) { + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer(); + + validateRequestBody(apiRequestBodyAsJson); + final JsonElement json = fromApiJsonHelper.parse(apiRequestBodyAsJson); + + ExternalAssetOwner owner = getOwner(json); + externalAssetOwnerTransfer.setOwnerId(owner.getId()); + externalAssetOwnerTransfer.setOwner(owner); + externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json)); + externalAssetOwnerTransfer.setStatus(status.name()); + externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json)); + externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json)); + externalAssetOwnerTransfer.setEffectiveDateFrom(ThreadLocalContextUtil.getBusinessDate()); + externalAssetOwnerTransfer.setEffectiveDateTo(LocalDate.of(9999, 12, 31)); + externalAssetOwnerTransfer.setLoanId(loanId); + externalAssetOwnerTransfer.setExternalLoanId(externalLoanId); + return externalAssetOwnerTransfer; + } + + private void validateRequestBody(String apiRequestBodyAsJson) { + final Set requestParameters = new HashSet<>( + Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, + ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, + ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE)); + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, apiRequestBodyAsJson, requestParameters); + } + + private LocalDate getSettlementDateFromJson(JsonElement json) { + String dateFormat = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.DATEFORMAT, json); + String locale = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.LOCALE, json); + return fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json, dateFormat, + JsonParserHelper.localeFromString(locale)); + } + + private ExternalId getTransferExternalIdFromJson(JsonElement json) { + String transferExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, json); + return ExternalIdFactory.produce(transferExternalId); + } + + private String getPurchasePriceRatioFromJson(JsonElement json) { + return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json); + } + + private ExternalAssetOwner getOwner(JsonElement json) { + String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json); + Optional byExternalId = externalAssetOwnerRepository + .findByExternalId(ExternalIdFactory.produce(ownerExternalId)); + return byExternalId.orElseGet(() -> createAndGetAssetOwner(ownerExternalId)); + } + + private ExternalAssetOwner createAndGetAssetOwner(String externalId) { + ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner(); + externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId)); + return externalAssetOwnerRepository.save(externalAssetOwner); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/SaleLoanToExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/SaleLoanToExternalAssetOwnerHandler.java new file mode 100644 index 000000000..39c56a1ef --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/SaleLoanToExternalAssetOwnerHandler.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.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 = "LOAN", action = "SALE") +public class SaleLoanToExternalAssetOwnerHandler implements NewCommandSourceHandler { + + private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return externalAssetOwnersWriteService.saleLoanByLoanId(command); + } +} 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 7f959be36..c7fa915cc 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 @@ -24,4 +24,7 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + + + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0003_asset_schemas.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0003_asset_schemas.xml index bd12acd21..6730c2e06 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0003_asset_schemas.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0003_asset_schemas.xml @@ -44,4 +44,24 @@ + + + + + + + + + + + + + + + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0004_change_purchase_price_ratio_type.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0004_change_purchase_price_ratio_type.xml new file mode 100644 index 000000000..f5caa3fe6 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0004_change_purchase_price_ratio_type.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0005_add_sale_and_buyback_command.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0005_add_sale_and_buyback_command.xml new file mode 100644 index 000000000..802fabc66 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0005_add_sale_and_buyback_command.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java new file mode 100644 index 000000000..40a0584f2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java @@ -0,0 +1,28 @@ +/** + * 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.cob.data; + +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +public interface LoanIdAndExternalId { + + Long getId(); + + ExternalId getExternalId(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/auditing/CustomAuditingHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/auditing/CustomAuditingHandler.java index 74910ae8b..e1268e9a1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/auditing/CustomAuditingHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/auditing/CustomAuditingHandler.java @@ -60,7 +60,12 @@ public class CustomAuditingHandler extends AuditingHandler { } private DateTimeProvider fetchDateTimeProvider(Object bean) { - return bean instanceof AbstractAuditableWithUTCDateTimeCustom ? CustomDateTimeProvider.TENANT : CustomDateTimeProvider.INSTANCE; + if (bean instanceof AbstractAuditableWithUTCDateTimeCustom + || bean instanceof org.apache.fineract.investor.domain.AbstractAuditableWithUTCDateTimeCustom) { + return CustomDateTimeProvider.TENANT; + } else { + return CustomDateTimeProvider.INSTANCE; + } } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java index e67504e77..f04af3057 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java @@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.core.domain; import javax.persistence.AttributeConverter; import javax.persistence.Converter; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; @Converter(autoApply = true) public class ExternalIdConverter implements AttributeConverter { @@ -32,7 +33,7 @@ public class ExternalIdConverter implements AttributeConverter, JpaSpecificat String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber"; + String GET_NON_CLOSED_LOAN_BY_LOAN_ID = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus in (100,200,300," + + "303,304)"; + + String EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID = "select case when (count (loan) > 0) then 'true' else 'false' end from Loan loan where loan.externalId = :externalLoanId and loan.loanStatus in (100,200,300,303,304)"; + String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId"; // should follow the logic of `FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE` query @@ -191,6 +196,12 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_BY_ACCOUNT_NUMBER) Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber); + @Query(GET_NON_CLOSED_LOAN_BY_LOAN_ID) + Loan getNonClosedLoanIdAndExternalIdByLoanId(@Param("loanId") Long loanId); + + @Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID) + boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId); + boolean existsByExternalId(@Param("externalId") ExternalId externalId); @Query(FIND_ALL_NON_CLOSED) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 497335802..f861c3869 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -262,4 +262,8 @@ public class LoanRepositoryWrapper { public List findLoanIdsByStatusId(Integer statusId) { return repository.findLoanIdByStatusId(statusId); } + + public Loan getNonClosedLoanIdAndExternalIdByLoanId(Long loanId) { + return repository.getNonClosedLoanIdAndExternalIdByLoanId(loanId); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 8ea66f57c..60754d1b3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -144,7 +145,7 @@ import org.springframework.util.CollectionUtils; @AllArgsConstructor @Service @Transactional(readOnly = true) -public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { +public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, LoanReadPlatformServiceCommon { private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date"; private final JdbcTemplate jdbcTemplate; @@ -591,6 +592,17 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } } + @Override + public LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId) { + Loan loan = loanRepositoryWrapper.getNonClosedLoanIdAndExternalIdByLoanId(loanId); + return new LoanIdAndExternalIdData(loan.getId(), loan.getExternalId()); + } + + @Override + public Long getLoanIdByLoanExternalId(String externalId) { + return loanRepositoryWrapper.findIdByExternalId(ExternalIdFactory.produce(externalId)); + } + private static final class LoanMapper implements RowMapper { private final DatabaseSpecificSQLGenerator sqlGenerator; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java new file mode 100644 index 000000000..26f9538af --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java @@ -0,0 +1,53 @@ +/** + * 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 io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import org.apache.fineract.integrationtests.client.IntegrationTest; +import org.apache.fineract.integrationtests.common.Utils; + +public class ExternalAssetOwnerHelper extends IntegrationTest { + + private final RequestSpecification requestSpec; + private final ResponseSpecification responseSpec; + + public ExternalAssetOwnerHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { + this.requestSpec = requestSpec; + this.responseSpec = responseSpec; + } + + public String initiateTransferByLoanId(Long loanId, String command, String json) { + final String INITIATE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers/loans/" + loanId + "?" + + Utils.TENANT_IDENTIFIER + "&command=" + command; + return Utils.performServerPost(requestSpec, responseSpec, INITIATE_TRANSFER_URL, json); + } + + public String retrieveTransferByTransferExternalId(String transferExternalId) { + final String RETRIEVE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers?" + Utils.TENANT_IDENTIFIER + + "&transferExternalId=" + transferExternalId; + return Utils.performServerGet(requestSpec, responseSpec, RETRIEVE_TRANSFER_URL); + } + + public String retrieveTransferByLoanId(Long loanId) { + final String RETRIEVE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers?" + Utils.TENANT_IDENTIFIER + + "&loanId=" + loanId; + return Utils.performServerGet(requestSpec, responseSpec, RETRIEVE_TRANSFER_URL); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java new file mode 100644 index 000000000..fe78adeab --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java @@ -0,0 +1,300 @@ +/** + * 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 com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CollateralManagementHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class InitiateExternalAssetOwnerTransferTest { + + private ResponseSpecification responseSpec; + private ResponseSpecification responseSpecError; + private RequestSpecification requestSpec; + private ExternalAssetOwnerHelper externalAssetOwnerHelper; + private LoanTransactionHelper loanTransactionHelper; + private LocalDate todaysDate; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + responseSpecError = new ResponseSpecBuilder().expectStatusCode(403).build(); + externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpec); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + + todaysDate = Utils.getLocalDateOfTenant(); + } + + @Test + public void saleActiveLoanToExternalAssetOwner() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2)); + GlobalConfigurationHelper.updateValueForGlobalConfiguration(requestSpec, responseSpec, "10", "0"); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + getSaleRequestJson("04 March 2020", transferExternalId)); + Type type = new TypeToken>() {}.getType(); + Map responseMap = new Gson().fromJson(saleResponse, type); + assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + + String retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + Type retrieveType = new TypeToken>>() {}.getType(); + List> retrieveResponseMap = new Gson().fromJson(retrieveResponse, retrieveType); + assertEquals(1, retrieveResponseMap.size()); + assertEquals(retrieveResponseMap.get(0).get("transferExternalId"), transferExternalId); + assertEquals(retrieveResponseMap.get(0).get("status"), "PENDING"); + } finally { + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + @Test + public void saleIsNotAllowedWhenTransferIsAlreadyPending() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2)); + GlobalConfigurationHelper.updateValueForGlobalConfiguration(requestSpec, responseSpec, "10", "0"); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + getSaleRequestJson("04 March 2020", transferExternalId)); + Type type = new TypeToken>() {}.getType(); + Map responseMap = new Gson().fromJson(saleResponse, type); + assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + + externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecError); + String errorResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + getSaleRequestJson("04 March 2020", transferExternalId)); + Map errorResponseMap = new Gson().fromJson(errorResponse, type); + assertEquals("External asset owner transfer is already in PENDING state for this loan.", + errorResponseMap.get("developerMessage")); + } finally { + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + @Test + public void saleAndBuybackOnTheSameDay() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2)); + GlobalConfigurationHelper.updateValueForGlobalConfiguration(requestSpec, responseSpec, "10", "0"); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + getSaleRequestJson("04 March 2020", transferExternalId)); + Type type = new TypeToken>() {}.getType(); + Map responseMap = new Gson().fromJson(saleResponse, type); + assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + + String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + String buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", + getSaleRequestJson("04 March 2020", buybackTransferExternalId)); + Map buybackResponseMap = new Gson().fromJson(buybackResponse, type); + assertEquals(buybackResponseMap.get("resourceExternalId"), buybackTransferExternalId); + + String retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + Type retrieveType = new TypeToken>>() {}.getType(); + List> retrieveResponseMap = new Gson().fromJson(retrieveResponse, retrieveType); + assertEquals(2, retrieveResponseMap.size()); + assertEquals(retrieveResponseMap.get(0).get("transferExternalId"), transferExternalId); + assertEquals(retrieveResponseMap.get(0).get("status"), "PENDING"); + assertEquals(retrieveResponseMap.get(1).get("transferExternalId"), buybackTransferExternalId); + assertEquals(retrieveResponseMap.get(1).get("status"), "BUYBACK"); + } finally { + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + private Integer createLoanProduct(final String chargeId) { + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() + .build(chargeId); + return loanTransactionHelper.getLoanProductId(loanProductJSON); + } + + private Integer applyForLoanApplication(final String clientID, final String loanProductID, final String savingsID, final String date) { + + List collaterals = new ArrayList<>(); + final Integer collateralId = CollateralManagementHelper.createCollateralProduct(requestSpec, responseSpec); + Assertions.assertNotNull(collateralId); + final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(requestSpec, responseSpec, clientID, + collateralId); + Assertions.assertNotNull(clientCollateralId); + addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1)); + + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("15,000.00").withLoanTermFrequency("4") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments() + .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate(date).withSubmittedOnDate(date).withCollaterals(collaterals) + .build(clientID, loanProductID, savingsID); + return loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + private void addCollaterals(List collaterals, Integer collateralId, BigDecimal quantity) { + collaterals.add(collaterals(collateralId, quantity)); + } + + private HashMap collaterals(Integer collateralId, BigDecimal quantity) { + HashMap collateral = new HashMap<>(2); + collateral.put("clientCollateralId", collateralId.toString()); + collateral.put("quantity", quantity.toString()); + return collateral; + } + + private String getSaleRequestJson(String date, String transferExternalId) { + final HashMap map = new HashMap<>(); + map.put("settlement_date", date); + map.put("owner_external_id", "1234567890987654321"); + map.put("transfer_external_id", transferExternalId); + map.put("purchase_price_ratio", "1.234"); + map.put("dateformat", "dd MMMM yyyy"); + map.put("locale", "en"); + return new Gson().toJson(map); + } + +}