FINERACT-1926: Fineract Asset Externalization - Cancel

- [x] Cancel API
- [x] Event raised on cancel
- [x] Integration test for cancel
This commit is contained in:
Janos Haber
2023-06-23 14:09:29 +02:00
committed by Adam Saghy
parent 90c10a5ade
commit 0079780e30
11 changed files with 676 additions and 5 deletions
@@ -41,6 +41,7 @@ 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.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
@@ -101,6 +102,35 @@ public class ExternalAssetOwnersApiResource {
return getResult(loanId, apiRequestBodyAsJson, commandParam);
}
@POST
@Path("/transfers/{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferResponse.class))),
@ApiResponse(responseCode = "403", description = "Transfer cannot be initiated") })
public String transferRequestWithId(@PathParam("id") final Long id,
@QueryParam("command") @Parameter(description = "command") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
platformUserRightsContext.isAuthenticated();
return getResultByTransferId(id, commandParam);
}
@POST
@Path("/transfers/external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ExternalAssetOwnersApiResourceSwagger.PostInitiateTransferResponse.class))),
@ApiResponse(responseCode = "403", description = "Transfer cannot be initiated") })
public String transferRequestWithId(@PathParam("externalId") final String externalId,
@QueryParam("command") @Parameter(description = "command") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
platformUserRightsContext.isAuthenticated();
Long id = externalAssetOwnersReadService.retrieveLastTransferIdByExternalId(new ExternalId(externalId));
return getResultByTransferId(id, commandParam);
}
@GET
@Path("/transfers")
@Produces({ MediaType.APPLICATION_JSON })
@@ -160,6 +190,18 @@ public class ExternalAssetOwnersApiResource {
}
private String getResultByTransferId(Long id, String command) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder();
CommandWrapper commandRequest;
if (CommandParameterUtil.is(command, "cancel")) {
commandRequest = builder.cancelTransactionByIdToExternalAssetOwner(id).build();
} else {
throw new UnrecognizedQueryParamException("command", command);
}
CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return postApiJsonSerializerService.serialize(result);
}
private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper commandRequest = null;
@@ -47,8 +47,12 @@ public interface ExternalAssetOwnerTransferRepository
@Query("select m.ownerTransfer.owner from ExternalAssetOwnerTransferLoanMapping m where m.loanId = :loanId")
Optional<ExternalAssetOwner> findActiveOwnerByLoanId(@Param("loanId") Long loanId);
@Query("SELECT t FROM ExternalAssetOwnerTransfer t WHERE t.loanId = :loanId AND t.effectiveDateTo > :effectiveDate")
List<ExternalAssetOwnerTransfer> findEffectiveTransfers(@Param("loanId") Long loanId, @Param("effectiveDate") LocalDate effectiveDate);
@Query("SELECT t FROM ExternalAssetOwnerTransfer t WHERE t.loanId = :loanId AND t.effectiveDateTo > :effectiveDate order by t.id desc")
List<ExternalAssetOwnerTransfer> findEffectiveTransfersOrderByIdDesc(@Param("loanId") Long loanId,
@Param("effectiveDate") LocalDate effectiveDate);
Optional<ExternalAssetOwnerTransfer> findFirstByExternalIdOrderByIdAsc(ExternalId externalTransferId);
@Query("select e.id from ExternalAssetOwnerTransfer e where e.externalId = :externalTransferId")
Optional<Long> findLastByExternalIdOrderByIdDesc(@Param("externalTransferId") ExternalId externalTransferId);
}
@@ -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 = "ASSET_OWNER_TRANSACTION", action = "CANCEL")
public class CancelLoanFromExternalAssetOwnerHandler implements NewCommandSourceHandler {
private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnersWriteService.cancelTransactionById(command);
}
}
@@ -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 = "CANCEL")
public class CancelTransactionFromExternalAssetOwnerHandler implements NewCommandSourceHandler {
private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnersWriteService.cancelTransactionById(command);
}
}
@@ -38,4 +38,6 @@ public interface ExternalAssetOwnersReadService {
ExternalOwnerJournalEntryData retrieveJournalEntriesOfOwner(String ownerExternalId, Integer offset, Integer limit);
ExternalTransferData retrieveFirstTransferByExternalId(ExternalId externalTransferId);
Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId);
}
@@ -120,6 +120,12 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe
.orElseThrow(() -> new ExternalAssetOwnerTransferNotFoundException(externalTransferId));
}
@Override
public Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId) {
return externalAssetOwnerTransferRepository.findLastByExternalIdOrderByIdDesc(externalTransferId)
.orElseThrow(() -> new ExternalAssetOwnerTransferNotFoundException(externalTransferId));
}
@Override
public ExternalTransferData retrieveTransferData(Long transferId) {
return externalAssetOwnerTransferRepository.findById(transferId).map(mapper::mapTransfer)
@@ -27,4 +27,6 @@ public interface ExternalAssetOwnersWriteService {
CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
CommandProcessingResult cancelTransactionById(JsonCommand command);
}
@@ -45,13 +45,18 @@ import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.investor.data.ExternalTransferRequestParameters;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
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.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -71,6 +76,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
private final FromJsonHelper fromApiJsonHelper;
private final LoanRepository loanRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Override
@Transactional
@@ -107,9 +113,23 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
}
}
@Override
public CommandProcessingResult cancelTransactionById(JsonCommand command) {
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = fetchAndValidateEffectiveTransferForCancel(command.entityId());
externalAssetOwnerTransfer.setEffectiveDateTo(DateUtils.getBusinessLocalDate());
ExternalAssetOwnerTransfer cancelTransfer = createCancelTransfer(externalAssetOwnerTransfer);
externalAssetOwnerTransferRepository.save(cancelTransfer);
externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
Loan loan = loanRepository.findById(externalAssetOwnerTransfer.getLoanId())
.orElseThrow(() -> new LoanNotFoundException(externalAssetOwnerTransfer.getLoanId()));
businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(cancelTransfer, loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
return buildResponseData(cancelTransfer);
}
private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
.findEffectiveTransfers(externalAssetOwnerTransfer.getLoanId(), externalAssetOwnerTransfer.getSettlementDate());
.findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
if (effectiveTransfers.size() == 2) {
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
@@ -128,8 +148,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
}
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(final Long loanId, final LocalDate settlementDate) {
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository.findEffectiveTransfers(loanId,
settlementDate);
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
.findEffectiveTransfersOrderByIdDesc(loanId, DateUtils.getBusinessLocalDate());
if (effectiveTransfers.size() == 0) {
throw new ExternalAssetOwnerInitiateTransferException(
@@ -150,6 +170,27 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
return effectiveTransfers.get(0);
}
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(final Long transferId) {
ExternalAssetOwnerTransfer selectedTransfer = externalAssetOwnerTransferRepository.findById(transferId)
.orElseThrow(() -> new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be cancelled, transfer with id %s does not exist", transferId)));
List<ExternalAssetOwnerTransfer> effective = externalAssetOwnerTransferRepository
.findEffectiveTransfersOrderByIdDesc(selectedTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
if (effective.isEmpty()) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be cancelled, there is no effective transfer for this loan"));
} else if (!Objects.equals(effective.get(0).getId(), selectedTransfer.getId())) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be cancelled, selected transfer is not the latest"));
} else if (selectedTransfer.getStatus() != ExternalTransferStatus.PENDING
&& selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be cancelled, the selected transfer status is not pending or buyback");
}
return selectedTransfer;
}
private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTransfer effectiveTransfer, LocalDate settlementDate,
ExternalId externalId) {
LocalDate effectiveDateFrom = DateUtils.getBusinessLocalDate();
@@ -168,6 +209,21 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
return externalAssetOwnerTransfer;
}
private ExternalAssetOwnerTransfer createCancelTransfer(ExternalAssetOwnerTransfer effectiveTransfer) {
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
externalAssetOwnerTransfer.setExternalId(effectiveTransfer.getExternalId());
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.CANCELLED);
externalAssetOwnerTransfer.setSubStatus(ExternalTransferSubStatus.USER_REQUESTED);
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
externalAssetOwnerTransfer.setSettlementDate(effectiveTransfer.getSettlementDate());
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveTransfer.getEffectiveDateFrom());
externalAssetOwnerTransfer.setEffectiveDateTo(effectiveTransfer.getEffectiveDateTo());
externalAssetOwnerTransfer.setPurchasePriceRatio(effectiveTransfer.getPurchasePriceRatio());
return externalAssetOwnerTransfer;
}
private CommandProcessingResult buildResponseData(ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer) {
return new CommandProcessingResultBuilder().withEntityId(savedExternalAssetOwnerTransfer.getId())
.withEntityExternalId(savedExternalAssetOwnerTransfer.getExternalId())