You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2023/06/23 12:09:34 UTC

[fineract] branch develop updated: FINERACT-1926: Fineract Asset Externalization - Cancel - [x] Cancel API - [x] Event raised on cancel - [x] Integration test for cancel

This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 0079780e3 FINERACT-1926: Fineract Asset Externalization - Cancel - [x] Cancel API - [x] Event raised on cancel - [x] Integration test for cancel
0079780e3 is described below

commit 0079780e3040ed6cee7239bee7149ee66d806369
Author: Janos Haber <ja...@finesolution.hu>
AuthorDate: Thu Jun 22 14:18:22 2023 +0200

    FINERACT-1926: Fineract Asset Externalization - Cancel
    - [x] Cancel API
    - [x] Event raised on cancel
    - [x] Integration test for cancel
---
 .../commands/service/CommandWrapperBuilder.java    |   8 +
 .../api/ExternalAssetOwnersApiResource.java        |  42 ++
 .../ExternalAssetOwnerTransferRepository.java      |   8 +-
 ...> CancelLoanFromExternalAssetOwnerHandler.java} |  17 +-
 ...lTransactionFromExternalAssetOwnerHandler.java} |  17 +-
 .../service/ExternalAssetOwnersReadService.java    |   2 +
 .../ExternalAssetOwnersReadServiceImpl.java        |   6 +
 .../service/ExternalAssetOwnersWriteService.java   |   2 +
 .../ExternalAssetOwnersWriteServiceImpl.java       |  62 ++-
 .../common/ExternalAssetOwnerHelper.java           |  12 +
 .../ExternalAssetOwnerTransferCancelTest.java      | 461 +++++++++++++++++++++
 11 files changed, 624 insertions(+), 13 deletions(-)

diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index ca182beff..77e4b893d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3638,4 +3638,12 @@ public class CommandWrapperBuilder {
         this.href = "/external-asset-owners/transfers/loans/" + loanId;
         return this;
     }
+
+    public CommandWrapperBuilder cancelTransactionByIdToExternalAssetOwner(final Long id) {
+        this.actionName = "CANCEL";
+        this.entityName = "ASSET_OWNER_TRANSACTION";
+        this.entityId = id;
+        this.href = "/external-asset-owners/transfers/" + id;
+        return this;
+    }
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java
index c8e2da270..2a773e939 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java
@@ -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;
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
index 54dc27d5f..c63e61ec1 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
@@ -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);
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java
similarity index 60%
copy from fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
copy to fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java
index ac73af9b9..7e40e3682 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java
@@ -18,13 +18,22 @@
  */
 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;
 
-public interface ExternalAssetOwnersWriteService {
+@RequiredArgsConstructor
+@Service
+@CommandType(entity = "ASSET_OWNER_TRANSACTION", action = "CANCEL")
+public class CancelLoanFromExternalAssetOwnerHandler implements NewCommandSourceHandler {
 
-    CommandProcessingResult saleLoanByLoanId(JsonCommand command);
-
-    CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
+    private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
 
+    @Override
+    public CommandProcessingResult processCommand(JsonCommand command) {
+        return externalAssetOwnersWriteService.cancelTransactionById(command);
+    }
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java
similarity index 60%
copy from fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
copy to fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java
index ac73af9b9..8200734b9 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java
@@ -18,13 +18,22 @@
  */
 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;
 
-public interface ExternalAssetOwnersWriteService {
+@RequiredArgsConstructor
+@Service
+@CommandType(entity = "LOAN", action = "CANCEL")
+public class CancelTransactionFromExternalAssetOwnerHandler implements NewCommandSourceHandler {
 
-    CommandProcessingResult saleLoanByLoanId(JsonCommand command);
-
-    CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
+    private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
 
+    @Override
+    public CommandProcessingResult processCommand(JsonCommand command) {
+        return externalAssetOwnersWriteService.cancelTransactionById(command);
+    }
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
index 61bb90259..919551e0c 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
@@ -38,4 +38,6 @@ public interface ExternalAssetOwnersReadService {
     ExternalOwnerJournalEntryData retrieveJournalEntriesOfOwner(String ownerExternalId, Integer offset, Integer limit);
 
     ExternalTransferData retrieveFirstTransferByExternalId(ExternalId externalTransferId);
+
+    Long retrieveLastTransferIdByExternalId(ExternalId externalTransferId);
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java
index 51b30fa30..8c86b932a 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java
@@ -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)
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
index ac73af9b9..8e5f7bdf0 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
@@ -27,4 +27,6 @@ public interface ExternalAssetOwnersWriteService {
 
     CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
 
+    CommandProcessingResult cancelTransactionById(JsonCommand command);
+
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
index d3db1547e..0f66961cd 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
@@ -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())
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java
index 0ce5a378b..ea7831136 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java
@@ -24,7 +24,9 @@ import org.apache.fineract.client.models.ExternalTransferData;
 import org.apache.fineract.client.models.PageExternalTransferData;
 import org.apache.fineract.client.models.PostInitiateTransferRequest;
 import org.apache.fineract.client.models.PostInitiateTransferResponse;
+import org.apache.fineract.client.util.Calls;
 import org.apache.fineract.integrationtests.client.IntegrationTest;
+import retrofit2.Response;
 
 public class ExternalAssetOwnerHelper extends IntegrationTest {
 
@@ -34,6 +36,16 @@ public class ExternalAssetOwnerHelper extends IntegrationTest {
         return ok(fineract().externalAssetOwners.transferRequestWithLoanId(loanId, request, command));
     }
 
+    public void cancelTransferByTransferExternalId(String transferExternalId) {
+        ok(fineract().externalAssetOwners.transferRequestWithId1(transferExternalId, "cancel"));
+    }
+
+    public void cancelTransferByTransferExternalIdError(String transferExternalId) {
+        Response<PostInitiateTransferResponse> response = Calls
+                .executeU(fineract().externalAssetOwners.transferRequestWithId1(transferExternalId, "cancel"));
+        assertThat(response.code()).isEqualTo(403);
+    }
+
     public PageExternalTransferData retrieveTransferByTransferExternalId(String transferExternalId) {
         return ok(fineract().externalAssetOwners.getTransfers(transferExternalId, null, null, 0, 100));
     }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferCancelTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferCancelTest.java
new file mode 100644
index 000000000..7617989ca
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferCancelTest.java
@@ -0,0 +1,461 @@
+/**
+ * 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.apache.fineract.client.models.ExternalTransferData.StatusEnum.BUYBACK;
+import static org.apache.fineract.client.models.ExternalTransferData.StatusEnum.CANCELLED;
+import static org.apache.fineract.client.models.ExternalTransferData.StatusEnum.PENDING;
+import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.accounting.common.AccountingConstants;
+import org.apache.fineract.client.models.ExternalOwnerTransferJournalEntryData;
+import org.apache.fineract.client.models.ExternalTransferData;
+import org.apache.fineract.client.models.GetFinancialActivityAccountsResponse;
+import org.apache.fineract.client.models.PageExternalTransferData;
+import org.apache.fineract.client.models.PostFinancialActivityAccountsRequest;
+import org.apache.fineract.client.models.PostInitiateTransferRequest;
+import org.apache.fineract.client.models.PostInitiateTransferResponse;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.BusinessStepHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
+import org.apache.fineract.integrationtests.common.ExternalAssetOwnerHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper;
+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.LoanTestLifecycleExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@SuppressWarnings("rawtypes")
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class ExternalAssetOwnerTransferCancelTest {
+
+    public String ownerExternalId;
+    private static ResponseSpecification RESPONSE_SPEC;
+    private static RequestSpecification REQUEST_SPEC;
+    private static Account ASSET_ACCOUNT;
+    private static Account FEE_PENALTY_ACCOUNT;
+    private static Account TRANSFER_ACCOUNT;
+    private static Account EXPENSE_ACCOUNT;
+    private static Account INCOME_ACCOUNT;
+    private static Account OVERPAYMENT_ACCOUNT;
+    private static FinancialActivityAccountHelper FINANCIAL_ACTIVITY_ACCOUNT_HELPER;
+    private static ExternalAssetOwnerHelper EXTERNAL_ASSET_OWNER_HELPER;
+    private static LoanTransactionHelper LOAN_TRANSACTION_HELPER;
+    private static SchedulerJobHelper SCHEDULER_JOB_HELPER;
+    private static LocalDate TODAYS_DATE;
+    private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter();
+
+    @BeforeAll
+    public static void setupInvestorBusinessStep() {
+        Utils.initializeRESTAssured();
+        REQUEST_SPEC = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build();
+        AccountHelper accountHelper = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC);
+        EXTERNAL_ASSET_OWNER_HELPER = new ExternalAssetOwnerHelper();
+        SCHEDULER_JOB_HELPER = new SchedulerJobHelper(REQUEST_SPEC);
+        FINANCIAL_ACTIVITY_ACCOUNT_HELPER = new FinancialActivityAccountHelper(REQUEST_SPEC);
+        LOAN_TRANSACTION_HELPER = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC);
+
+        TODAYS_DATE = Utils.getLocalDateOfTenant();
+        new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
+                "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                "EXTERNAL_ASSET_OWNER_TRANSFER");
+
+        ASSET_ACCOUNT = accountHelper.createAssetAccount();
+        FEE_PENALTY_ACCOUNT = accountHelper.createAssetAccount();
+        TRANSFER_ACCOUNT = accountHelper.createAssetAccount();
+        EXPENSE_ACCOUNT = accountHelper.createExpenseAccount();
+        INCOME_ACCOUNT = accountHelper.createIncomeAccount();
+        OVERPAYMENT_ACCOUNT = accountHelper.createLiabilityAccount();
+
+        setProperFinancialActivity(TRANSFER_ACCOUNT);
+    }
+
+    private static void setProperFinancialActivity(Account transferAccount) {
+        List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts();
+        financialMappings.forEach(mapping -> FINANCIAL_ACTIVITY_ACCOUNT_HELPER.deleteFinancialActivityAccount(mapping.getId()));
+        FINANCIAL_ACTIVITY_ACCOUNT_HELPER.createFinancialActivityAccount(new PostFinancialActivityAccountsRequest()
+                .financialActivityId((long) AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue())
+                .glAccountId((long) transferAccount.getAccountID()));
+    }
+
+    @Test
+    public void successCancelSale() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, RESPONSE_SPEC,
+                    GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+            addPenaltyForLoan(loanID, "10");
+
+            PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02");
+            validateResponse(saleTransferResponse, loanID);
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02",
+                            "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+
+            PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue());
+            retrieveResponse.getContent().forEach(transfer -> getAndValidateThereIsNoJournalEntriesForTransfer(transfer.getTransferId()));
+
+            EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalId(saleTransferResponse.getResourceExternalId());
+
+            // updateBusinessDateAndExecuteCOBJob("2020-03-03");
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02",
+                            "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
+    private void getAndValidateThereIsNoJournalEntriesForTransfer(Long transferId) {
+        ExternalOwnerTransferJournalEntryData result = EXTERNAL_ASSET_OWNER_HELPER.retrieveJournalEntriesOfTransfer(transferId);
+        assertNull(result.getJournalEntryData());
+    }
+
+    @Test
+    public void saleAndBuybackOnTheSameDay() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, RESPONSE_SPEC,
+                    GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+
+            PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02");
+            validateResponse(saleTransferResponse, loanID);
+            PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-02");
+            validateResponse(buybackTransferResponse, loanID);
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02",
+                            "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+            getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId());
+            getAndValidateThereIsNoActiveMapping(buybackTransferResponse.getResourceExternalId());
+
+            EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalIdError(saleTransferResponse.getResourceExternalId());
+
+            EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalId(buybackTransferResponse.getResourceExternalId());
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02",
+                            "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+
+            EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalId(saleTransferResponse.getResourceExternalId());
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02",
+                            "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), "2020-03-02",
+                            "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+            getAndValidateThereIsNoActiveMapping((long) loanID);
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
+    private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) {
+        String transferExternalId = UUID.randomUUID().toString();
+        ownerExternalId = UUID.randomUUID().toString();
+        return createSaleTransfer(loanID, settlementDate, transferExternalId, ownerExternalId, "1.0");
+    }
+
+    private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId,
+            String ownerExternalId, String purchasePriceRatio) {
+        PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
+                new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
+                        .transferExternalId(transferExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio));
+        assertEquals(transferExternalId, saleResponse.getResourceExternalId());
+        return saleResponse;
+    }
+
+    private PostInitiateTransferResponse createBuybackTransfer(Integer loanID, String settlementDate) {
+        String transferExternalId = UUID.randomUUID().toString();
+        PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "buyback",
+                new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
+                        .transferExternalId(transferExternalId));
+        assertEquals(transferExternalId, saleResponse.getResourceExternalId());
+        return saleResponse;
+    }
+
+    private void addPenaltyForLoan(Integer loanID, String amount) {
+        // Add Charge Penalty
+        Integer penalty = ChargesHelper.createCharges(REQUEST_SPEC, RESPONSE_SPEC,
+                ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, amount, true));
+        Integer penalty1LoanChargeId = this.LOAN_TRANSACTION_HELPER.addChargesForLoan(loanID,
+                LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "02 March 2020", amount));
+        assertNotNull(penalty1LoanChargeId);
+    }
+
+    private void setInitialBusinessDate(String date) {
+        GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE);
+        BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, BUSINESS_DATE, LocalDate.parse(date));
+        GlobalConfigurationHelper.updateValueForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "10", "0");
+    }
+
+    private void cleanUpAndRestoreBusinessDate() {
+        REQUEST_SPEC = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        REQUEST_SPEC.header("Fineract-Platform-TenantId", "default");
+        RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build();
+        BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, BUSINESS_DATE, TODAYS_DATE);
+        GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE);
+    }
+
+    @NotNull
+    private Integer createClient() {
+        final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
+        Assertions.assertNotNull(clientID);
+        return clientID;
+    }
+
+    @NotNull
+    private Integer createLoanForClient(Integer clientID) {
+        Integer overdueFeeChargeId = ChargesHelper.createCharges(REQUEST_SPEC, RESPONSE_SPEC,
+                ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1"));
+        Assertions.assertNotNull(overdueFeeChargeId);
+
+        Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString());
+        Assertions.assertNotNull(loanProductID);
+        HashMap loanStatusHashMap;
+
+        Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), "10 January 2020");
+
+        Assertions.assertNotNull(loanID);
+
+        loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+        LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+        loanStatusHashMap = LOAN_TRANSACTION_HELPER.approveLoan("01 March 2020", loanID);
+        LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
+        String loanDetails = LOAN_TRANSACTION_HELPER.getLoanDetails(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+        loanStatusHashMap = LOAN_TRANSACTION_HELPER.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID,
+                JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+        return loanID;
+    }
+
+    private Integer createLoanProduct(final String chargeId) {
+
+        final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4")
+                .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
+                .withAccountingRulePeriodicAccrual(new Account[] { ASSET_ACCOUNT, EXPENSE_ACCOUNT, INCOME_ACCOUNT, OVERPAYMENT_ACCOUNT })
+                .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                .withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).build(chargeId);
+        return LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON);
+    }
+
+    private Integer applyForLoanApplication(final String clientID, final String loanProductID, final String date) {
+        List<HashMap> collaterals = new ArrayList<>();
+        Integer collateralId = CollateralManagementHelper.createCollateralProduct(REQUEST_SPEC, RESPONSE_SPEC);
+        Assertions.assertNotNull(collateralId);
+        Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(REQUEST_SPEC, RESPONSE_SPEC, clientID, collateralId);
+        Assertions.assertNotNull(clientCollateralId);
+        addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
+
+        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, null);
+        return LOAN_TRANSACTION_HELPER.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 void getAndValidateExternalAssetOwnerTransferByLoan(Integer loanID, ExpectedExternalTransferData... expectedItems) {
+        PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue());
+        assertEquals(expectedItems.length, retrieveResponse.getNumberOfElements());
+
+        for (ExpectedExternalTransferData expected : expectedItems) {
+            assertNotNull(retrieveResponse.getContent());
+            Optional<ExternalTransferData> first = retrieveResponse.getContent().stream()
+                    .filter(e -> Objects.equals(e.getTransferExternalId(), expected.transferExternalId)
+                            && Objects.equals(e.getStatus(), expected.status))
+                    .findFirst();
+            assertTrue(first.isPresent());
+            ExternalTransferData etd = first.get();
+            assertEquals(expected.transferExternalId, etd.getTransferExternalId());
+            assertEquals(expected.status, etd.getStatus());
+            assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate());
+            assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom());
+            assertEquals(LocalDate.parse(expected.effectiveTo), etd.getEffectiveTo());
+            if (!expected.detailsExpected) {
+                assertNull(etd.getDetails());
+            } else {
+                assertNotNull(etd.getDetails());
+                assertEquals(expected.totalOutstanding, etd.getDetails().getTotalOutstanding());
+                assertEquals(expected.totalPrincipalOutstanding, etd.getDetails().getTotalPrincipalOutstanding());
+                assertEquals(expected.totalInterestOutstanding, etd.getDetails().getTotalInterestOutstanding());
+                assertEquals(expected.totalPenaltyOutstanding, etd.getDetails().getTotalPenaltyChargesOutstanding());
+                assertEquals(expected.totalFeeOutstanding, etd.getDetails().getTotalFeeChargesOutstanding());
+                assertEquals(expected.totalOverpaid, etd.getDetails().getTotalOverpaid());
+            }
+        }
+    }
+
+    private void getAndValidateThereIsNoActiveMapping(Long loanId) {
+        ExternalTransferData activeTransfer = EXTERNAL_ASSET_OWNER_HELPER.retrieveActiveTransferByLoanId(loanId);
+        assertNull(activeTransfer);
+    }
+
+    private void getAndValidateThereIsNoActiveMapping(String transferExternalId) {
+        ExternalTransferData activeTransfer = EXTERNAL_ASSET_OWNER_HELPER.retrieveActiveTransferByTransferExternalId(transferExternalId);
+        assertNull(activeTransfer);
+    }
+
+    private void validateResponse(PostInitiateTransferResponse transferResponse, Integer loanID) {
+        assertNotNull(transferResponse);
+        assertNotNull(transferResponse.getResourceId());
+        assertNotNull(transferResponse.getResourceExternalId());
+        assertNotNull(transferResponse.getSubResourceId());
+        assertEquals((long) loanID, transferResponse.getSubResourceId());
+        assertNotNull(transferResponse.getSubResourceExternalId());
+        assertNull(transferResponse.getChanges());
+    }
+
+    @RequiredArgsConstructor()
+    public static class ExpectedExternalTransferData {
+
+        private final ExternalTransferData.StatusEnum status;
+
+        private final String transferExternalId;
+
+        private final String settlementDate;
+
+        private final String effectiveFrom;
+        private final String effectiveTo;
+        private final boolean detailsExpected;
+        private final BigDecimal totalOutstanding;
+        private final BigDecimal totalPrincipalOutstanding;
+        private final BigDecimal totalInterestOutstanding;
+        private final BigDecimal totalPenaltyOutstanding;
+        private final BigDecimal totalFeeOutstanding;
+        private final BigDecimal totalOverpaid;
+
+        static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId,
+                String settlementDate, String effectiveFrom, String effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding,
+                BigDecimal totalPrincipalOutstanding, BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding,
+                BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) {
+            return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, detailsExpected,
+                    totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding, totalFeeOutstanding,
+                    totalOverpaid);
+        }
+
+    }
+
+    @RequiredArgsConstructor()
+    public static class ExpectedJournalEntryData {
+
+        private final Long glAccountId;
+        private final Long entryTypeId;
+        private final BigDecimal amount;
+        private final LocalDate transactionDate;
+        private final LocalDate submittedOnDate;
+
+        static ExpectedJournalEntryData expected(Long glAccountId, Long entryTypeId, BigDecimal amount, LocalDate transactionDate,
+                LocalDate submittedOnDate) {
+            return new ExpectedJournalEntryData(glAccountId, entryTypeId, amount, transactionDate, submittedOnDate);
+        }
+
+    }
+}