[FINERACT-1926] POST API for asset externalization

This commit is contained in:
taskain7
2023-05-24 11:26:54 +02:00
committed by Adam Saghy
parent fd27401de4
commit 045f0ec121
40 changed files with 1257 additions and 25 deletions
@@ -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<ExternalTransferData> apiJsonSerializerService;
private final DefaultToApiJsonSerializer<ExternalTransferResponseData> 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);
}
}
@@ -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<GetExternalTransferPageItems> 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;
}
}
}
@@ -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;
}
@@ -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;
@@ -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";
}
@@ -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;
}
@@ -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 {
@@ -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<ExternalAssetOwner, Long>, JpaSpecificationExecutor<ExternalAssetOwner> {
Optional<ExternalAssetOwner> findByExternalId(ExternalId externalId);
}
@@ -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;
@@ -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<ExternalAssetOwnerTransfer, Long>, JpaSpecificationExecutor<ExternalAssetOwnerTransfer> {
@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<ExternalAssetOwnerTransfer> findAllByIncomingId(@Param("loanId") Long loanId, @Param("loanExternalId") String loanExternalId,
@Param("transferExternalId") String transferExternalId);
List<ExternalAssetOwnerTransfer> findAllByLoanId(Long loanId);
List<ExternalAssetOwnerTransfer> findAllByExternalLoanId(ExternalId externalLoanId);
List<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> findLatestByLoanId(@Param("loanId") Long loanId);
}
@@ -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<ExternalId, String> {
@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);
}
}
@@ -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);
}
}
@@ -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<ExternalAssetOwnerInitiateTransferException> {
@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();
}
}
@@ -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);
}
}
@@ -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<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId) {
List<ExternalAssetOwnerTransfer> result = externalAssetOwnerTransferRepository.findAllByIncomingId(loanId, externalLoanId,
transferExternalId);
List<ExternalAssetOwnerTransfer> 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();
}
}
@@ -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);
}
@@ -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<String, Object> 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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> 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<String> 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<Map<String, Object>>() {}.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<ExternalAssetOwner> 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);
}
}
@@ -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);
}
}
@@ -24,4 +24,7 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<include relativeToChangelogFile="true" file="parts/0001_initial_schema.xml"/>
<include relativeToChangelogFile="true" file="parts/0002_asset_schemas.xml"/>
<include relativeToChangelogFile="true" file="parts/0003_asset_schemas.xml"/>
<include relativeToChangelogFile="true" file="parts/0004_change_purchase_price_ratio_type.xml"/>
<include relativeToChangelogFile="true" file="parts/0005_add_sale_and_buyback_command.xml"/>
</databaseChangeLog>
@@ -44,4 +44,24 @@
</changeSet>
<changeSet author="fineract" id="3" context="mysql">
<addColumn tableName="m_external_asset_owner_transfer">
<column name="external_loan_id" type="varchar(100)"/>
<column name="loan_id" type="bigint" remarks="Loan ID">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="4" context="mysql">
<addForeignKeyConstraint baseColumnNames="loan_id"
baseTableName="m_external_asset_owner_transfer"
constraintName="FK_external_asset_owner_transfer_loan_id" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_loan" validate="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,36 @@
<?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 author="fineract" id="1" context="postgresql">
<modifyDataType columnName="purchase_price_ratio"
newDataType="VARCHAR(50)"
tableName="m_external_asset_owner_transfer"/>
</changeSet>
<changeSet author="fineract" id="2" context="mysql">
<modifyDataType columnName="purchase_price_ratio"
newDataType="VARCHAR(50)"
tableName="m_external_asset_owner_transfer"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,41 @@
<?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="transaction_loan"/>
<column name="code" value="SALE_LOAN"/>
<column name="entity_name" value="LOAN"/>
<column name="action_name" value="SALE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="transaction_loan"/>
<column name="code" value="BUYBACK_LOAN"/>
<column name="entity_name" value="LOAN"/>
<column name="action_name" value="BUYBACK"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>