[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
@@ -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;
}
}
@@ -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;
}
@@ -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);
}
@@ -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>
@@ -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();
}
@@ -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;
}
}
/**
@@ -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<ExternalId, String> {
@@ -32,7 +33,7 @@ public class ExternalIdConverter implements AttributeConverter<ExternalId, Strin
@Override
public ExternalId convertToEntityAttribute(String externalId) {
return StringUtils.isBlank(externalId) ? ExternalId.empty() : new ExternalId(externalId);
return StringUtils.isBlank(externalId) ? ExternalId.empty() : ExternalIdFactory.produce(externalId);
}
}
@@ -85,6 +85,11 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, 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<Loan, Long>, 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)
@@ -262,4 +262,8 @@ public class LoanRepositoryWrapper {
public List<Long> findLoanIdsByStatusId(Integer statusId) {
return repository.findLoanIdByStatusId(statusId);
}
public Loan getNonClosedLoanIdAndExternalIdByLoanId(Long loanId) {
return repository.getNonClosedLoanIdAndExternalIdByLoanId(loanId);
}
}
@@ -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<LoanAccountData> {
private final DatabaseSpecificSQLGenerator sqlGenerator;
@@ -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);
}
}
@@ -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<Map<String, Object>>() {}.getType();
Map<String, Object> responseMap = new Gson().fromJson(saleResponse, type);
assertEquals(responseMap.get("resourceExternalId"), transferExternalId);
String retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
Type retrieveType = new TypeToken<List<Map<String, Object>>>() {}.getType();
List<Map<String, Object>> 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<Map<String, Object>>() {}.getType();
Map<String, Object> 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<String, Object> 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<Map<String, Object>>() {}.getType();
Map<String, Object> 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<String, Object> buybackResponseMap = new Gson().fromJson(buybackResponse, type);
assertEquals(buybackResponseMap.get("resourceExternalId"), buybackTransferExternalId);
String retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
Type retrieveType = new TypeToken<List<Map<String, Object>>>() {}.getType();
List<Map<String, Object>> 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<HashMap> 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<HashMap> collaterals, Integer collateralId, BigDecimal quantity) {
collaterals.add(collaterals(collateralId, quantity));
}
private HashMap<String, String> collaterals(Integer collateralId, BigDecimal quantity) {
HashMap<String, String> 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<String, String> 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);
}
}