diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java index 871be9ab2..985c0b083 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java @@ -18,11 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.service; -import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; - public interface LoanReadPlatformServiceCommon { - LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId); - Long getLoanIdByLoanExternalId(String externalId); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java index e347aeb3d..5e5ac6273 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -53,36 +53,27 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep List transferDataList = externalAssetOwnerTransferRepository.findAll( (root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId), criteriaBuilder.equal(root.get("settlementDate"), settlementDate), - root.get("status").in( - List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK))), + root.get("status").in(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK))), Sort.by(Sort.Direction.ASC, "id")); int size = transferDataList.size(); - if (size > 2) { - throw new IllegalStateException( - String.format("Found too many owner transfers(%s) by the settlement date(%s)", size, settlementDate)); - } else if (size == 2) { + if (size == 2) { ExternalTransferStatus firstTransferStatus = transferDataList.get(0).getStatus(); ExternalTransferStatus secondTransferStatus = transferDataList.get(1).getStatus(); - if (!ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) { - throw new IllegalStateException(String.format("Illegal transfer found. Expected %s, found: %s", - ExternalTransferStatus.BUYBACK, secondTransferStatus)); - } - - switch (firstTransferStatus) { - case PENDING -> handleSameDaySaleAndBuyback(settlementDate, transferDataList); - case ACTIVE -> handleBuyback(loan, settlementDate, transferDataList); - default -> throw new IllegalStateException(String.format("Illegal transfer found. Expected %s or %s, found: %s", - ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE, firstTransferStatus)); + if (!ExternalTransferStatus.PENDING.equals(firstTransferStatus) + || !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) { + throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s", + ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus)); } + handleSameDaySaleAndBuyback(settlementDate, transferDataList); } else if (size == 1) { - ExternalAssetOwnerTransfer externalAssetOwnerTransfer = transferDataList.get(0); - if (!ExternalTransferStatus.PENDING.equals(externalAssetOwnerTransfer.getStatus())) { - throw new IllegalStateException(String.format("Illegal transfer found. Expected %s, found: %s", - ExternalTransferStatus.PENDING, externalAssetOwnerTransfer.getStatus())); + ExternalAssetOwnerTransfer transfer = transferDataList.get(0); + if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) { + handleSale(loan, settlementDate, transfer); + } else if (ExternalTransferStatus.BUYBACK.equals(transfer.getStatus())) { + handleBuyback(loan, settlementDate, transfer); } - handleSale(loan, settlementDate, externalAssetOwnerTransfer); } log.debug("end processing loan ownership transfer business step for loan Id [{}]", loan.getId()); @@ -95,20 +86,25 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep } private void handleBuyback(final Loan loan, final LocalDate settlementDate, - final List externalAssetOwnerTransferList) { - ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, externalAssetOwnerTransferList); + final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) { + ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository + .findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()), + criteriaBuilder.equal(root.get("ownerId"), buybackExternalAssetOwnerTransfer.getOwnerId()), + criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE), + criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31))) + .orElseThrow(); + ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer, + activeExternalAssetOwnerTransfer); // TODO: trigger asset loan transfer executed event } private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final LocalDate settlementDate, - List externalAssetOwnerTransferList) { - ExternalAssetOwnerTransfer saleExternalAssetOwnerTransfer = externalAssetOwnerTransferList.get(0); - ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferList.get(1); - saleExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate); + ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer, ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer) { + activeExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate); buybackExternalAssetOwnerTransfer.setEffectiveDateTo(buybackExternalAssetOwnerTransfer.getEffectiveDateFrom()); - externalAssetOwnerTransferRepository.save(saleExternalAssetOwnerTransfer); + externalAssetOwnerTransferRepository.save(activeExternalAssetOwnerTransfer); buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer); - externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), buybackExternalAssetOwnerTransfer); + externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), activeExternalAssetOwnerTransfer); // TODO: create asset ownership accounting entries // TODO: create asset ownership transaction entries return buybackExternalAssetOwnerTransfer; @@ -161,6 +157,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) { ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer(); newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner()); + newExternalAssetOwnerTransfer.setOwnerId(externalAssetOwnerTransfer.getOwnerId()); newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId()); newExternalAssetOwnerTransfer.setStatus(status); newExternalAssetOwnerTransfer.setSubStatus(subStatus); 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 270525ab9..b4ad2e54c 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 @@ -18,6 +18,8 @@ */ package org.apache.fineract.investor.domain; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.domain.Page; @@ -39,4 +41,6 @@ public interface ExternalAssetOwnerTransferRepository @Query("select e from ExternalAssetOwnerTransfer e where e.loanId = :loanId and e.id = (select max(ex.id) from ExternalAssetOwnerTransfer ex where ex.loanId = :loanId)") Optional findLatestByLoanId(@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); } 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 1dc7c4196..25df924a7 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 @@ -33,16 +33,17 @@ import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; 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.investor.data.ExternalTransferRequestParameters; @@ -53,7 +54,9 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,36 +64,96 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService { + private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31); + private static final List NON_CLOSED_LOAN_STATUSES = List.of(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED, + LoanStatus.ACTIVE, LoanStatus.TRANSFER_IN_PROGRESS, LoanStatus.TRANSFER_ON_HOLD); + private static final List BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING, + ExternalTransferStatus.ACTIVE); private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; private final ExternalAssetOwnerRepository externalAssetOwnerRepository; private final FromJsonHelper fromApiJsonHelper; - private final LoanReadPlatformServiceCommon loanReadPlatformService; + private final LoanRepository loanRepository; @Override @Transactional public CommandProcessingResult saleLoanByLoanId(JsonCommand command) { + final JsonElement json = fromApiJsonHelper.parse(command.json()); Long loanId = command.getLoanId(); - LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId); - validateLoanStatus(loanIdAndExternalId); - ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(loanId, command.json(), loanIdAndExternalId.getLoanExternalId(), - ExternalTransferStatus.PENDING); + LocalDate settlementDate = getSettlementDateFromJson(json); + LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId); + validateLoanStatus(loanIdAndExternalIdAndStatus); + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createSaleTransfer(loanId, command.json(), + loanIdAndExternalIdAndStatus.getExternalId()); validateSale(externalAssetOwnerTransfer); - ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); - return buildResponseData(savedExternalAssetOwnerTransfer); + externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); + return buildResponseData(externalAssetOwnerTransfer); } @Override @Transactional public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) { + final JsonElement json = fromApiJsonHelper.parse(command.json()); Long loanId = command.getLoanId(); - LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId); - validateLoanStatus(loanIdAndExternalId); - ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(loanId, command.json(), loanIdAndExternalId.getLoanExternalId(), - ExternalTransferStatus.BUYBACK); - validateBuyBack(externalAssetOwnerTransfer); - ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); - return buildResponseData(savedExternalAssetOwnerTransfer); + LocalDate settlementDate = getSettlementDateFromJson(json); + ExternalId externalId = getTransferExternalIdFromJson(json); + LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId); + validateLoanStatus(loanIdAndExternalIdAndStatus); + validateSettlementDate(settlementDate); + ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanId, settlementDate); + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createBuybackTransfer(effectiveTransfer, settlementDate, externalId); + externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); + return buildResponseData(externalAssetOwnerTransfer); + } + + private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + List effectiveTransfers = externalAssetOwnerTransferRepository + .findEffectiveTransfers(externalAssetOwnerTransfer.getLoanId(), externalAssetOwnerTransfer.getSettlementDate()); + + if (effectiveTransfers.size() > 0) { + throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer"); + } + } + + private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(final Long loanId, final LocalDate settlementDate) { + List effectiveTransfers = externalAssetOwnerTransferRepository.findEffectiveTransfers(loanId, + settlementDate); + + if (effectiveTransfers.size() == 0) { + throw new ExternalAssetOwnerInitiateTransferException( + "This loan cannot be bought back, it is not owned by an external asset owner"); + } else if (effectiveTransfers.size() == 2) { + throw new ExternalAssetOwnerInitiateTransferException( + "This loan cannot be bought back, external asset owner buyback transfer is already in progress"); + } else if (!BUYBACK_READY_STATUSES.contains(effectiveTransfers.get(0).getStatus())) { + throw new ExternalAssetOwnerInitiateTransferException( + String.format("This loan cannot be bought back, effective transfer is not in right state: %s", + effectiveTransfers.get(0).getStatus())); + } else if (settlementDate.isBefore(effectiveTransfers.get(0).getSettlementDate())) { + throw new ExternalAssetOwnerInitiateTransferException( + String.format("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s", + effectiveTransfers.get(0).getSettlementDate())); + } + + return effectiveTransfers.get(0); + } + + private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTransfer effectiveTransfer, LocalDate settlementDate, + ExternalId externalId) { + LocalDate effectiveDateFrom = DateUtils.getBusinessLocalDate(); + + ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer(); + externalAssetOwnerTransfer.setExternalId(externalId); + externalAssetOwnerTransfer.setOwnerId(effectiveTransfer.getOwnerId()); + externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.BUYBACK); + externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId()); + externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId()); + externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner()); + externalAssetOwnerTransfer.setSettlementDate(settlementDate); + externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom); + externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31); + externalAssetOwnerTransfer.setPurchasePriceRatio(effectiveTransfer.getPurchasePriceRatio()); + return externalAssetOwnerTransfer; } private CommandProcessingResult buildResponseData(ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer) { @@ -98,8 +161,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW changes.put(ExternalTransferRequestParameters.SETTLEMENT_DATE, savedExternalAssetOwnerTransfer.getSettlementDate()); changes.put(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue()); - changes.put(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, - savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue()); + changes.put(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, savedExternalAssetOwnerTransfer.getExternalId().getValue()); changes.put(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, savedExternalAssetOwnerTransfer.getPurchasePriceRatio()); return new CommandProcessingResultBuilder().withEntityId(savedExternalAssetOwnerTransfer.getId()) .withEntityExternalId(savedExternalAssetOwnerTransfer.getExternalId()) @@ -112,11 +174,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { validateSettlementDate(externalAssetOwnerTransfer); validateTransferStatusForSale(externalAssetOwnerTransfer); - } - - private void validateBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { - validateSettlementDate(externalAssetOwnerTransfer); - validateTransferStatusForBuyBack(externalAssetOwnerTransfer); + validateEffectiveTransferForSale(externalAssetOwnerTransfer); } private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { @@ -125,9 +183,14 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW } } - private void validateLoanStatus(LoanIdAndExternalIdData loanIdAndExternalIdAndExternalId) { - if (Objects.isNull(loanIdAndExternalIdAndExternalId.getLoanId()) - && Objects.isNull(loanIdAndExternalIdAndExternalId.getLoanExternalId())) { + private void validateSettlementDate(LocalDate settlementDate) { + if (settlementDate.isBefore(ThreadLocalContextUtil.getBusinessDate())) { + throw new ExternalAssetOwnerInitiateTransferException("Settlement date cannot be in the past"); + } + } + + private void validateLoanStatus(LoanIdAndExternalIdAndStatus entity) { + if (!NON_CLOSED_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus()))) { throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status"); } } @@ -140,52 +203,35 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus(); if (latestTransferStatus.equals(ExternalTransferStatus.PENDING)) { throw new ExternalAssetOwnerInitiateTransferException( - "External asset owner transfer is already in PENDING state for this loan."); + "External asset owner transfer is already in PENDING state for this loan"); } else if (latestTransferStatus.equals(ExternalTransferStatus.ACTIVE)) { throw new ExternalAssetOwnerInitiateTransferException( - "This loan cannot be sold, because it is owned by an external asset owner."); + "This loan cannot be sold, because it is owned by an external asset owner"); } } } - private void validateTransferStatusForBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { - Optional latestTransferOptional = externalAssetOwnerTransferRepository - .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId()); - if (latestTransferOptional.isEmpty()) { - throw new ExternalAssetOwnerInitiateTransferException( - "This loan cannot be bought back, because it is not owned by an external asset owner"); - } else { - ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get(); - ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus(); - if (latestTransferStatus.equals(ExternalTransferStatus.BUYBACK)) { - throw new ExternalAssetOwnerInitiateTransferException( - "External asset owner transfer is already in BUYBACK state for this loan."); - } - } - } - - private ExternalAssetOwnerTransfer parseJson(Long loanId, String apiRequestBodyAsJson, ExternalId externalLoanId, - ExternalTransferStatus status) { + private ExternalAssetOwnerTransfer createSaleTransfer(Long loanId, String apiRequestBodyAsJson, ExternalId externalLoanId) { ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer(); - - validateRequestBody(apiRequestBodyAsJson); + LocalDate effectiveFrom = ThreadLocalContextUtil.getBusinessDate(); + validateSaleRequestBody(apiRequestBodyAsJson); final JsonElement json = fromApiJsonHelper.parse(apiRequestBodyAsJson); ExternalAssetOwner owner = getOwner(json); externalAssetOwnerTransfer.setOwnerId(owner.getId()); externalAssetOwnerTransfer.setOwner(owner); externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json)); - externalAssetOwnerTransfer.setStatus(status); + externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING); externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json)); externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json)); - externalAssetOwnerTransfer.setEffectiveDateFrom(ThreadLocalContextUtil.getBusinessDate()); - externalAssetOwnerTransfer.setEffectiveDateTo(LocalDate.of(9999, 12, 31)); + externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom); + externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31); externalAssetOwnerTransfer.setLoanId(loanId); externalAssetOwnerTransfer.setExternalLoanId(externalLoanId); return externalAssetOwnerTransfer; } - private void validateRequestBody(String apiRequestBodyAsJson) { + private void validateSaleRequestBody(String apiRequestBodyAsJson) { final Set requestParameters = new HashSet<>( Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, @@ -248,4 +294,10 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId)); return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner); } + + private LoanIdAndExternalIdAndStatus fetchLoanDetails(Long loanId) { + Optional loanIdAndExternalIdAndStatusResult = loanRepository + .findLoanIdAndExternalIdAndStatusByLoanId(loanId); + return loanIdAndExternalIdAndStatusResult.orElseThrow(() -> new LoanNotFoundException(loanId)); + } } diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java index e4f8e7d9c..9492e630b 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java @@ -33,6 +33,7 @@ import java.time.ZoneId; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; @@ -80,7 +81,6 @@ public class LoanAccountOwnerTransferBusinessStepTest { // given final Loan loanForProcessing = Mockito.mock(Loan.class); Long loanId = 1L; - LocalDate settlementDate = actualDate; when(loanForProcessing.getId()).thenReturn(loanId); // when final Loan processedLoan = underTest.execute(loanForProcessing); @@ -90,33 +90,13 @@ public class LoanAccountOwnerTransferBusinessStepTest { } @Test - public void givenLoanTooManyTransfer() { - // given - final Loan loanForProcessing = Mockito.mock(Loan.class); - Long loanId = 1L; - LocalDate settlementDate = actualDate; - when(loanForProcessing.getId()).thenReturn(loanId); - ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - ExternalAssetOwnerTransfer thirdResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - List response = List.of(firstResponseItem, secondResponseItem, thirdResponseItem); - when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) - .thenReturn(response); - // when - IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing)); - // then - assertEquals("Found too many owner transfers(3) by the settlement date(" + actualDate + ")", exception.getMessage()); - verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); - - } - - @Test - public void givenLoanTwoTransferButInvalidSecondTransfer() { + public void givenLoanTwoTransferButInvalidTransfers() { // given final Loan loanForProcessing = Mockito.mock(Loan.class); when(loanForProcessing.getId()).thenReturn(1L); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE); List response = List.of(firstResponseItem, secondResponseItem); when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) @@ -124,26 +104,7 @@ public class LoanAccountOwnerTransferBusinessStepTest { // when IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing)); // then - assertEquals("Illegal transfer found. Expected BUYBACK, found: ACTIVE", exception.getMessage()); - verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); - } - - @Test - public void givenLoanTwoTransferButInvalidFirstTransfer() { - // given - final Loan loanForProcessing = Mockito.mock(Loan.class); - when(loanForProcessing.getId()).thenReturn(1L); - ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); - when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); - List response = List.of(firstResponseItem, secondResponseItem); - when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) - .thenReturn(response); - // when - IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing)); - // then - assertEquals("Illegal transfer found. Expected PENDING or ACTIVE, found: BUYBACK", exception.getMessage()); + assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage()); verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); } @@ -208,11 +169,12 @@ public class LoanAccountOwnerTransferBusinessStepTest { when(loanForProcessing.getId()).thenReturn(1L); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE); - when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); - List response = List.of(firstResponseItem, secondResponseItem); + when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); + when(firstResponseItem.getEffectiveDateFrom()).thenReturn(actualDate); + List response = List.of(firstResponseItem); when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) .thenReturn(response); + when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.of(secondResponseItem)); ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor .forClass(ExternalAssetOwnerTransfer.class); when(externalAssetOwnerTransferRepository.save(firstResponseItem)).thenReturn(firstResponseItem); @@ -221,31 +183,14 @@ public class LoanAccountOwnerTransferBusinessStepTest { final Loan processedLoan = underTest.execute(loanForProcessing); // then verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); - verify(firstResponseItem).setEffectiveDateTo(actualDate); + verify(firstResponseItem).setEffectiveDateTo(firstResponseItem.getEffectiveDateFrom()); verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture()); - verify(secondResponseItem).setEffectiveDateTo(secondResponseItem.getEffectiveDateFrom()); + verify(secondResponseItem).setEffectiveDateTo(actualDate); verify(externalAssetOwnerTransferLoanMappingRepository, times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem); assertEquals(processedLoan, loanForProcessing); } - @Test - public void givenLoanOneTransferButInvalidTransfer() { - // given - final Loan loanForProcessing = Mockito.mock(Loan.class); - when(loanForProcessing.getId()).thenReturn(1L); - ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE); - List response = List.of(firstResponseItem); - when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) - .thenReturn(response); - // when - IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing)); - // then - assertEquals("Illegal transfer found. Expected PENDING, found: ACTIVE", exception.getMessage()); - verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); - } - @Test public void givenLoanSale() { // given @@ -291,6 +236,86 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertEquals(processedLoan, loanForProcessing); } + @Test + public void givenLoanSaleButBalanceIsZero() { + // given + final Loan loanForProcessing = Mockito.mock(Loan.class); + when(loanForProcessing.getId()).thenReturn(1L); + ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); + List response = List.of(firstResponseItem); + when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) + .thenReturn(response); + ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransfer.class); + ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer); + LoanSummary loanSummary = Mockito.mock(LoanSummary.class); + when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); + when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO); + when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO); + // when + final Loan processedLoan = underTest.execute(loanForProcessing); + // then + verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + verify(firstResponseItem).setEffectiveDateTo(actualDate); + verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture()); + + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId()); + assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); + assertEquals(processedLoan, loanForProcessing); + } + + @Test + public void givenLoanSaleButBalanceIsNegative() { + // given + final Loan loanForProcessing = Mockito.mock(Loan.class); + when(loanForProcessing.getId()).thenReturn(1L); + ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); + List response = List.of(firstResponseItem); + when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) + .thenReturn(response); + ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransfer.class); + ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer); + LoanSummary loanSummary = Mockito.mock(LoanSummary.class); + when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); + when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate()); + when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate()); + // when + final Loan processedLoan = underTest.execute(loanForProcessing); + // then + verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + verify(firstResponseItem).setEffectiveDateTo(actualDate); + verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture()); + + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId()); + assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); + assertEquals(processedLoan, loanForProcessing); + } + @Test public void testGetEnumStyledNameSuccessScenario() { final String actualEnumName = underTest.getEnumStyledName(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java similarity index 83% rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java index f09191a3a..92c272939 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java @@ -16,16 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.infrastructure.core.data; +package org.apache.fineract.cob.data; import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; import org.apache.fineract.infrastructure.core.domain.ExternalId; -@Data @AllArgsConstructor -public class LoanIdAndExternalIdData { +@Getter +public class LoanIdAndExternalIdAndStatus { - private Long loanId; - private ExternalId loanExternalId; + Long id; + ExternalId externalId; + Integer loanStatus; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java similarity index 97% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 3f7952d11..70081a7b7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Optional; import org.apache.fineract.cob.data.LoanCOBParameter; import org.apache.fineract.cob.data.LoanIdAndExternalIdAndAccountNo; +import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus; import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.jpa.repository.JpaRepository; @@ -86,8 +87,7 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber"; - String GET_NON_CLOSED_LOAN_BY_LOAN_ID = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus in (100,200,300," - + "303,304)"; + String FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS = "select new org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus(loan.id, loan.externalId, loan.loanStatus) from Loan loan where loan.id = :loanId"; String EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID = "select case when (count (loan) > 0) then 'true' else 'false' end from Loan loan where loan.externalId = :externalLoanId and loan.loanStatus in (100,200,300,303,304)"; @@ -197,8 +197,8 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_BY_ACCOUNT_NUMBER) Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber); - @Query(GET_NON_CLOSED_LOAN_BY_LOAN_ID) - Optional getNonClosedLoanIdAndExternalIdByLoanId(@Param("loanId") Long loanId); + @Query(FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS) + Optional findLoanIdAndExternalIdAndStatusByLoanId(@Param("loanId") Long loanId); @Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID) boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java deleted file mode 100644 index 40a0584f2..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.cob.data; - -import org.apache.fineract.infrastructure.core.domain.ExternalId; - -public interface LoanIdAndExternalId { - - Long getId(); - - ExternalId getExternalId(); -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index f072a5073..959d71eda 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -264,10 +263,4 @@ public class LoanRepositoryWrapper { return repository.findLoanIdByStatusId(statusId); } - public Optional getNonClosedLoanIdAndExternalIdByLoanId(Long loanId) { - if (repository.findById(loanId).isEmpty()) { - throw new LoanNotFoundException(loanId); - } - return repository.getNonClosedLoanIdAndExternalIdByLoanId(loanId); - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index d9f15f469..710fa04fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -42,7 +42,6 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -593,13 +592,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa } } - @Override - public LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId) { - Optional loan = loanRepositoryWrapper.getNonClosedLoanIdAndExternalIdByLoanId(loanId); - return loan.map(value -> new LoanIdAndExternalIdData(value.getId(), value.getExternalId())) - .orElseGet(() -> new LoanIdAndExternalIdData(null, null)); - } - @Override public Long getLoanIdByLoanExternalId(String externalId) { ExternalId loanExternalId = ExternalIdFactory.produce(externalId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java index 1e11607d5..2ef563e77 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java @@ -43,12 +43,15 @@ import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuil import org.apache.fineract.integrationtests.common.loans.LoanCOBCatchUpHelper; 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.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +@ExtendWith(LoanTestLifecycleExtension.class) public class LoanCOBAccountLockCatchupInlineCOBTest { private ResponseSpecification responseSpec; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java new file mode 100644 index 000000000..0f30e6fe1 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java @@ -0,0 +1,44 @@ +/** + * 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.common; + +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.client.models.BusinessStep; +import org.apache.fineract.client.models.UpdateBusinessStepConfigRequest; +import org.apache.fineract.integrationtests.client.IntegrationTest; + +public class BusinessStepHelper extends IntegrationTest { + + public BusinessStepHelper() {} + + public void updateSteps(String jobName, String... steps) { + long order = 0; + List stepList = new ArrayList<>(); + for (String step : steps) { + order++; + BusinessStep businessStep = new BusinessStep(); + businessStep.stepName(step); + businessStep.setOrder(order); + stepList.add(businessStep); + } + ok(fineract().businessStepConfiguration.updateJobBusinessStepConfig(jobName, + new UpdateBusinessStepConfigRequest().businessSteps(stepList))); + } +} 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 new file mode 100644 index 000000000..d114e124f --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java @@ -0,0 +1,49 @@ +/** + * 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.common; + +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.integrationtests.client.IntegrationTest; + +public class ExternalAssetOwnerHelper extends IntegrationTest { + + public ExternalAssetOwnerHelper() {} + + public PostInitiateTransferResponse initiateTransferByLoanId(Long loanId, String command, PostInitiateTransferRequest request) { + return ok(fineract().externalAssetOwners.transferRequestWithLoanId(loanId, request, command)); + } + + public PageExternalTransferData retrieveTransferByTransferExternalId(String transferExternalId) { + return ok(fineract().externalAssetOwners.getTransfer1(transferExternalId, null, null, 0, 100)); + } + + public PageExternalTransferData retrieveTransferByLoanExternalId(String loanExternalId) { + return ok(fineract().externalAssetOwners.getTransfer1(null, null, loanExternalId, 0, 100)); + } + + public PageExternalTransferData retrieveTransferByLoanId(Long loanId) { + return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, 0, 100)); + } + + public PageExternalTransferData retrieveTransferByLoanId(Long loanId, int offset, int limit) { + return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, offset, limit)); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java deleted file mode 100644 index 2270f8f1c..000000000 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.integrationtests.investor.externalassetowner; - -import io.restassured.specification.RequestSpecification; -import io.restassured.specification.ResponseSpecification; -import org.apache.fineract.client.models.PageExternalTransferData; -import org.apache.fineract.integrationtests.client.IntegrationTest; -import org.apache.fineract.integrationtests.common.Utils; - -public class ExternalAssetOwnerHelper extends IntegrationTest { - - private final RequestSpecification requestSpec; - private final ResponseSpecification responseSpec; - - public ExternalAssetOwnerHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { - this.requestSpec = requestSpec; - this.responseSpec = responseSpec; - } - - public String initiateTransferByLoanId(Long loanId, String command, String json) { - final String INITIATE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers/loans/" + loanId + "?" - + Utils.TENANT_IDENTIFIER + "&command=" + command; - return Utils.performServerPost(requestSpec, responseSpec, INITIATE_TRANSFER_URL, json); - } - - public String initiateTransferByLoanExternalId(String loanExternalId, String command, String json) { - final String INITIATE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers/loans/external-id/" + loanExternalId - + "?" + Utils.TENANT_IDENTIFIER + "&command=" + command; - return Utils.performServerPost(requestSpec, responseSpec, INITIATE_TRANSFER_URL, json); - } - - public String retrieveTransferByTransferExternalId(String transferExternalId) { - final String RETRIEVE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers?" + Utils.TENANT_IDENTIFIER - + "&transferExternalId=" + transferExternalId; - return Utils.performServerGet(requestSpec, responseSpec, RETRIEVE_TRANSFER_URL); - } - - public PageExternalTransferData retrieveTransferByLoanId(Long loanId) { - return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, 0, 100)); - } - - public PageExternalTransferData retrieveTransferByLoanId(Long loanId, int offset, int limit) { - return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, offset, limit)); - } -} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java index 8887bb386..a218ab0da 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java @@ -19,65 +19,79 @@ package org.apache.fineract.integrationtests.investor.externalassetowner; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; -import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.UUID; 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.CallFailedRuntimeException; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; 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.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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +@ExtendWith(LoanTestLifecycleExtension.class) public class InitiateExternalAssetOwnerTransferTest { private ResponseSpecification responseSpec; - private ResponseSpecification responseSpecError; - private ResponseSpecification responseSpecNotFound; private RequestSpecification requestSpec; private ExternalAssetOwnerHelper externalAssetOwnerHelper; private LoanTransactionHelper loanTransactionHelper; + private SchedulerJobHelper schedulerJobHelper; private LocalDate todaysDate; + @BeforeAll + public static void setupInvestorBusinessStep() { + 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"); + } + @BeforeEach public void setup() { Utils.initializeRESTAssured(); requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - responseSpecError = new ResponseSpecBuilder().expectStatusCode(403).build(); - responseSpecNotFound = new ResponseSpecBuilder().expectStatusCode(404).build(); - externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpec); + externalAssetOwnerHelper = new ExternalAssetOwnerHelper(); loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + schedulerJobHelper = new SchedulerJobHelper(requestSpec); todaysDate = Utils.getLocalDateOfTenant(); } @Test - public void saleActiveLoanToExternalAssetOwner() { + public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() { try { GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); @@ -110,18 +124,96 @@ public class InitiateExternalAssetOwnerTransferTest { JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); - String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", - getSaleRequestJson("04 March 2020", transferExternalId)); - Type type = new TypeToken>() {}.getType(); - Map responseMap = new Gson().fromJson(saleResponse, type); - assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + String transferExternalId = UUID.randomUUID().toString(); + PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")); + assertEquals(transferExternalId, saleResponse.getResourceExternalId()); PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); - List retrieveResponseMap = retrieveResponse.getContent(); - assertEquals(1, retrieveResponse.getTotalElements()); - assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), transferExternalId); - assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.PENDING); + + assertEquals(1, retrieveResponse.getNumberOfElements()); + ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 3)); + // Run the Loan COB Job + final String jobName = "Loan COB"; + schedulerJobHelper.executeAndAwaitJob(jobName); + + retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + + assertEquals(2, retrieveResponse.getNumberOfElements()); + externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + + String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", + new PostInitiateTransferRequest().settlementDate("2020-03-03").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(buybackTransferExternalId)); + + assertEquals(buybackResponse.getResourceExternalId(), buybackTransferExternalId); + + retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + + assertEquals(3, retrieveResponse.getNumberOfElements()); + externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(2); + assertEquals(buybackTransferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 4)); + + schedulerJobHelper.executeAndAwaitJob(jobName); + + retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + assertEquals(3, retrieveResponse.getNumberOfElements()); + externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(2); + assertEquals(buybackTransferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveTo()); } finally { requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); @@ -165,19 +257,17 @@ public class InitiateExternalAssetOwnerTransferTest { JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); - String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", - getSaleRequestJson("04 March 2020", transferExternalId)); - Type type = new TypeToken>() {}.getType(); - Map responseMap = new Gson().fromJson(saleResponse, type); - assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + String transferExternalId = UUID.randomUUID().toString(); + PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")); + assertEquals(transferExternalId, saleResponse.getResourceExternalId()); - externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecError); - String errorResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", - getSaleRequestJson("04 March 2020", transferExternalId)); - Map errorResponseMap = new Gson().fromJson(errorResponse, type); - assertEquals("External asset owner transfer is already in PENDING state for this loan.", - errorResponseMap.get("developerMessage")); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"))); + assertTrue(exception.getMessage().contains("External asset owner transfer is already in PENDING state for this loan")); } finally { requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); @@ -226,13 +316,12 @@ public class InitiateExternalAssetOwnerTransferTest { loanTransactionHelper.makeRepayment("04 March 2020", 16000.0f, loanID); - externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecError); String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", - getSaleRequestJson("05 March 2020", transferExternalId)); - Type type = new TypeToken>() {}.getType(); - Map errorResponseMap = new Gson().fromJson(saleResponse, type); - assertEquals("Loan is not in active status", errorResponseMap.get("developerMessage")); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"))); + assertTrue(exception.getMessage().contains("Loan is not in active status")); } finally { requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); @@ -278,58 +367,147 @@ public class InitiateExternalAssetOwnerTransferTest { LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", - getSaleRequestJson("04 March 2020", transferExternalId)); - Type type = new TypeToken>() {}.getType(); - Map responseMap = new Gson().fromJson(saleResponse, type); - assertEquals(responseMap.get("resourceExternalId"), transferExternalId); + PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")); + assertEquals(transferExternalId, saleResponse.getResourceExternalId()); - String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", - getSaleRequestJson("04 March 2020", buybackTransferExternalId)); - Map buybackResponseMap = new Gson().fromJson(buybackResponse, type); - assertEquals(buybackResponseMap.get("resourceExternalId"), buybackTransferExternalId); + PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", + new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId)); - PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue(), 0, 1); - List retrieveResponseMap = retrieveResponse.getContent(); + assertEquals(buybackResponse.getResourceExternalId(), transferExternalId); - assertEquals(2, retrieveResponse.getTotalElements()); - assertEquals(1, retrieveResponse.getNumberOfElements()); - assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), transferExternalId); - assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.PENDING); + PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + assertEquals(2, retrieveResponse.getNumberOfElements()); + ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); - retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue(), 1, 1); - retrieveResponseMap = retrieveResponse.getContent(); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 3)); + final String jobName = "Loan COB"; + schedulerJobHelper.executeAndAwaitJob(jobName); - assertEquals(2, retrieveResponse.getTotalElements()); - assertEquals(1, retrieveResponse.getNumberOfElements()); - assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), buybackTransferExternalId); - assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.BUYBACK); + retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + assertEquals(4, retrieveResponse.getNumberOfElements()); + externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(2); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.CANCELLED, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(3); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.CANCELLED, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo()); } finally { requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); requestSpec.header("Fineract-Platform-TenantId", "default"); responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate); GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); } } @Test - public void getNotFoundErrorIfTheLoanDoesNotExistWithTheGivenID() { + public void saleAndBuybackMultipleTimes() { try { GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2)); GlobalConfigurationHelper.updateValueForGlobalConfiguration(requestSpec, responseSpec, "10", "0"); - externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecNotFound); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; - String nonExistingLoanExternalID = "NonExistingLoanExternalID"; - String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanExternalId(nonExistingLoanExternalID, "sale", - getSaleRequestJson("05 March 2020", transferExternalId)); - Type type = new TypeToken>() {}.getType(); - Map errorResponseMap = new Gson().fromJson(saleResponse, type); - assertEquals("The requested resource is not available.", errorResponseMap.get("developerMessage")); + PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")); + + assertEquals(transferExternalId, saleResponse.getResourceExternalId()); + + String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e"; + PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", + new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(buybackTransferExternalId)); + + assertEquals(buybackResponse.getResourceExternalId(), buybackTransferExternalId); + PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue()); + assertEquals(2, retrieveResponse.getNumberOfElements()); + ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 4), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + externalTransferData = retrieveResponse.getContent().get(1); + assertEquals(transferExternalId, externalTransferData.getTransferExternalId()); + assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus()); + assertEquals(LocalDate.of(2020, 3, 4), externalTransferData.getSettlementDate()); + assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom()); + assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo()); + + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale", + new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"))); + + assertTrue(exception.getMessage().contains("This loan cannot be sold, there is already an in progress transfer")); + + CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class, + () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback", + new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en") + .transferExternalId(buybackTransferExternalId))); + assertTrue(exception2.getMessage() + .contains("This loan cannot be bought back, external asset owner buyback transfer is already in progress")); } finally { requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); @@ -378,15 +556,4 @@ public class InitiateExternalAssetOwnerTransferTest { return collateral; } - private String getSaleRequestJson(String date, String transferExternalId) { - final HashMap map = new HashMap<>(); - map.put("settlementDate", date); - map.put("ownerExternalId", "1234567890987654321"); - map.put("transferExternalId", transferExternalId); - map.put("purchasePriceRatio", "1.234"); - map.put("dateFormat", "dd MMMM yyyy"); - map.put("locale", "en"); - return new Gson().toJson(map); - } - }