From 0079780e3040ed6cee7239bee7149ee66d806369 Mon Sep 17 00:00:00 2001 From: Janos Haber Date: Thu, 22 Jun 2023 14:18:22 +0200 Subject: [PATCH] FINERACT-1926: Fineract Asset Externalization - Cancel - [x] Cancel API - [x] Event raised on cancel - [x] Integration test for cancel --- .../service/CommandWrapperBuilder.java | 8 + .../api/ExternalAssetOwnersApiResource.java | 42 ++ .../ExternalAssetOwnerTransferRepository.java | 8 +- ...ncelLoanFromExternalAssetOwnerHandler.java | 39 ++ ...nsactionFromExternalAssetOwnerHandler.java | 39 ++ .../ExternalAssetOwnersReadService.java | 2 + .../ExternalAssetOwnersReadServiceImpl.java | 6 + .../ExternalAssetOwnersWriteService.java | 2 + .../ExternalAssetOwnersWriteServiceImpl.java | 62 ++- .../common/ExternalAssetOwnerHelper.java | 12 + .../ExternalAssetOwnerTransferCancelTest.java | 461 ++++++++++++++++++ 11 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferCancelTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 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 findActiveOwnerByLoanId(@Param("loanId") Long loanId); - @Query("SELECT t FROM ExternalAssetOwnerTransfer t WHERE t.loanId = :loanId AND t.effectiveDateTo > :effectiveDate") - List 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 findEffectiveTransfersOrderByIdDesc(@Param("loanId") Long loanId, + @Param("effectiveDate") LocalDate effectiveDate); Optional findFirstByExternalIdOrderByIdAsc(ExternalId externalTransferId); + + @Query("select e.id from ExternalAssetOwnerTransfer e where e.externalId = :externalTransferId") + Optional findLastByExternalIdOrderByIdDesc(@Param("externalTransferId") ExternalId externalTransferId); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java new file mode 100644 index 000000000..7e40e3682 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelLoanFromExternalAssetOwnerHandler.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.investor.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@CommandType(entity = "ASSET_OWNER_TRANSACTION", action = "CANCEL") +public class CancelLoanFromExternalAssetOwnerHandler implements NewCommandSourceHandler { + + 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/CancelTransactionFromExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java new file mode 100644 index 000000000..8200734b9 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/CancelTransactionFromExternalAssetOwnerHandler.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.investor.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@CommandType(entity = "LOAN", action = "CANCEL") +public class CancelTransactionFromExternalAssetOwnerHandler implements NewCommandSourceHandler { + + 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 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 effectiveTransfers = externalAssetOwnerTransferRepository.findEffectiveTransfers(loanId, - settlementDate); + List 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 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 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 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 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 collaterals, Integer collateralId, BigDecimal quantity) { + collaterals.add(collaterals(collateralId, quantity)); + } + + private HashMap collaterals(Integer collateralId, BigDecimal quantity) { + HashMap collateral = new HashMap<>(2); + collateral.put("clientCollateralId", collateralId.toString()); + collateral.put("quantity", quantity.toString()); + return collateral; + } + + private 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 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); + } + + } +}