From d639b20af16c61ebdec168cad29c78f88ab94405 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Fri, 26 May 2023 16:40:28 +0200 Subject: [PATCH] FINERACT-1926: Asset transfer to external owner COB business step --- .../cob/exceptions/BusinessStepException.java | 0 .../LoanAccountOwnerTransferBusinessStep.java | 195 +++++++++++ .../data/ExternalTransferSubStatus.java | 23 ++ .../domain/ExternalAssetOwnerTransfer.java | 27 +- ...ExternalAssetOwnerTransferLoanMapping.java | 5 +- ...setOwnerTransferLoanMappingRepository.java | 28 ++ .../ExternalAssetOwnerTransferRepository.java | 1 + ...alAssetOwnerTransferNotFoundException.java | 33 ++ ...backLoanFromExternalAssetOwnerHandler.java | 2 +- .../ExternalAssetOwnersReadService.java | 1 + .../ExternalAssetOwnersWriteService.java | 3 +- .../ExternalAssetOwnersWriteServiceImpl.java | 14 +- .../investor/module-changelog-master.xml | 1 + .../investor/parts/0006_asset_schemas.xml | 49 +++ ...nAccountOwnerTransferBusinessStepTest.java | 307 ++++++++++++++++++ 15 files changed, 670 insertions(+), 19 deletions(-) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java (100%) create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java create mode 100644 fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml create mode 100644 fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java b/fineract-core/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java rename to fineract-core/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java 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 new file mode 100644 index 000000000..e347aeb3d --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -0,0 +1,195 @@ +/** + * 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.cob.loan; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.loan.LoanCOBBusinessStep; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.investor.data.ExternalTransferStatus; +import org.apache.fineract.investor.data.ExternalTransferSubStatus; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep { + + public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31); + private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; + private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; + + @Override + public Loan execute(Loan loan) { + Long loanId = loan.getId(); + log.debug("start processing loan ownership transfer business step for loan with Id [{}]", loanId); + + LocalDate settlementDate = DateUtils.getBusinessLocalDate(); + 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))), + 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) { + 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)); + } + } 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())); + } + handleSale(loan, settlementDate, externalAssetOwnerTransfer); + } + + log.debug("end processing loan ownership transfer business step for loan Id [{}]", loan.getId()); + return loan; + } + + private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer); + // TODO: trigger asset loan transfer executed event + } + + private void handleBuyback(final Loan loan, final LocalDate settlementDate, + final List externalAssetOwnerTransferList) { + ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, externalAssetOwnerTransferList); + // 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); + buybackExternalAssetOwnerTransfer.setEffectiveDateTo(buybackExternalAssetOwnerTransfer.getEffectiveDateFrom()); + externalAssetOwnerTransferRepository.save(saleExternalAssetOwnerTransfer); + buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer); + externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), buybackExternalAssetOwnerTransfer); + // TODO: create asset ownership accounting entries + // TODO: create asset ownership transaction entries + return buybackExternalAssetOwnerTransfer; + } + + private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate settlementDate, + ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer; + if (isTransferable(loan)) { + newExternalAssetOwnerTransfer = createActiveEntry(settlementDate, externalAssetOwnerTransfer); + createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer); + // TODO: create asset ownership accounting entries + // TODO: create asset ownership transaction entries + } else { + ExternalTransferSubStatus subStatus = ExternalTransferSubStatus.BALANCE_ZERO; + if (loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0) { + subStatus = ExternalTransferSubStatus.BALANCE_NEGATIVE; + } + newExternalAssetOwnerTransfer = createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.DECLINED, + subStatus, settlementDate, settlementDate); + } + return newExternalAssetOwnerTransfer; + } + + private void createActiveMapping(Long loanId, ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + ExternalAssetOwnerTransferLoanMapping externalAssetOwnerTransferLoanMapping = new ExternalAssetOwnerTransferLoanMapping(); + externalAssetOwnerTransferLoanMapping.setLoanId(loanId); + externalAssetOwnerTransferLoanMapping.setOwnerTransfer(externalAssetOwnerTransfer); + externalAssetOwnerTransferLoanMappingRepository.save(externalAssetOwnerTransferLoanMapping); + } + + private boolean isTransferable(final Loan loan) { + return loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0; + } + + private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List transferDataList) { + ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0)); + ExternalAssetOwnerTransfer cancelledBuybackTransfer = cancelTransfer(settlementDate, transferDataList.get(1)); + // TODO: trigger asset loan transfer cancel events + } + + private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate, + final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED, + ExternalTransferSubStatus.SAMEDAY_TRANSFERS, settlementDate, settlementDate); + } + + private ExternalAssetOwnerTransfer createNewEntry(final LocalDate settlementDate, + final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status, + final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) { + ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer(); + newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner()); + newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId()); + newExternalAssetOwnerTransfer.setStatus(status); + newExternalAssetOwnerTransfer.setSubStatus(subStatus); + newExternalAssetOwnerTransfer.setSettlementDate(settlementDate); + newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId()); + newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId()); + newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio()); + newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom); + newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo); + + externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate); + externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); + return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer); + } + + private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate settlementDate, + final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + LocalDate effectiveFrom = settlementDate.plusDays(1); + return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.ACTIVE, null, effectiveFrom, + FUTURE_DATE_9999_12_31); + } + + @Override + public String getEnumStyledName() { + return "EXTERNAL_ASSET_OWNER_TRANSFER"; + } + + @Override + public String getHumanReadableName() { + return "Execute external asset owner transfer"; + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java new file mode 100644 index 000000000..7899b49a8 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.investor.data; + +public enum ExternalTransferSubStatus { + BALANCE_ZERO, BALANCE_NEGATIVE, SAMEDAY_TRANSFERS, USER_REQUESTED +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java index 618c11ecb..2179dbff1 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java @@ -21,6 +21,8 @@ package org.apache.fineract.investor.domain; import java.time.LocalDate; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; @@ -29,6 +31,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.investor.data.ExternalTransferStatus; +import org.apache.fineract.investor.data.ExternalTransferSubStatus; @Getter @Setter @@ -37,29 +41,34 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; @Entity public class ExternalAssetOwnerTransfer extends AbstractAuditableWithUTCDateTimeCustom { - @Column(name = "owner_id") + @Column(name = "owner_id", nullable = false) private Long ownerId; @ManyToOne - @JoinColumn(name = "owner_id", insertable = false, updatable = false) + @JoinColumn(name = "owner_id", insertable = false, updatable = false, nullable = false) private ExternalAssetOwner owner; - @Column(name = "external_id", length = 100) + @Column(name = "external_id", length = 100, nullable = false) private ExternalId externalId; - @Column(name = "status", length = 50) - private String status; + @Column(name = "status", length = 50, nullable = false) + @Enumerated(EnumType.STRING) + private ExternalTransferStatus status; - @Column(name = "purchase_price_ratio", length = 50) + @Column(name = "sub_status", length = 50) + @Enumerated(EnumType.STRING) + private ExternalTransferSubStatus subStatus; + + @Column(name = "purchase_price_ratio", length = 50, nullable = false) private String purchasePriceRatio; - @Column(name = "settlement_date") + @Column(name = "settlement_date", nullable = false) private LocalDate settlementDate; - @Column(name = "effective_date_from") + @Column(name = "effective_date_from", nullable = false) private LocalDate effectiveDateFrom; - @Column(name = "effective_date_to") + @Column(name = "effective_date_to", nullable = false) private LocalDate effectiveDateTo; @Column(name = "loan_id", nullable = false) diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java index f94683a24..eef0b33dc 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java @@ -23,7 +23,6 @@ import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -32,7 +31,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa @Getter @Setter @Table(name = "m_external_asset_owner_transfer_loan_mapping") -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @Entity public class ExternalAssetOwnerTransferLoanMapping extends AbstractAuditableWithUTCDateTimeCustom { @@ -40,7 +39,7 @@ public class ExternalAssetOwnerTransferLoanMapping extends AbstractAuditableWith private Long loanId; @ManyToOne - @JoinColumn(name = "owner_transfer_id") + @JoinColumn(name = "owner_transfer_id", nullable = false) private ExternalAssetOwnerTransfer ownerTransfer; } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java new file mode 100644 index 000000000..e7b28b4d8 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.investor.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ExternalAssetOwnerTransferLoanMappingRepository extends JpaRepository, + JpaSpecificationExecutor { + + void deleteByLoanIdAndOwnerTransfer(Long loanId, ExternalAssetOwnerTransfer externalAssetOwnerTransfer); +} 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 799143d0a..d6fe00f09 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 @@ -37,4 +37,5 @@ 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); + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java new file mode 100644 index 000000000..985517a4d --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.investor.exception; + +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; +import org.apache.fineract.investor.data.ExternalTransferStatus; + +public class ExternalAssetOwnerTransferNotFoundException extends AbstractPlatformResourceNotFoundException { + + public ExternalAssetOwnerTransferNotFoundException(ExternalId externalId, ExternalTransferStatus externalTransferStatus) { + super("error.msg.external.asset.owner.transfer.external.id.and.status", + String.format("External asset owner transfer with external id: %s and status: %s does not found", externalId.getValue(), + externalTransferStatus), + externalId.getValue(), externalTransferStatus); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java index 9fc11ca38..00977500c 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java @@ -34,6 +34,6 @@ public class BuybackLoanFromExternalAssetOwnerHandler implements NewCommandSourc @Override public CommandProcessingResult processCommand(JsonCommand command) { - return externalAssetOwnersWriteService.buyBackLoanByLoanId(command); + return externalAssetOwnersWriteService.buybackLoanByLoanId(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 8f5f56d85..bf011d6ca 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 @@ -24,4 +24,5 @@ import org.apache.fineract.investor.data.ExternalTransferData; public interface ExternalAssetOwnersReadService { List retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId); + } 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 b3133875d..ac73af9b9 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 @@ -25,5 +25,6 @@ public interface ExternalAssetOwnersWriteService { CommandProcessingResult saleLoanByLoanId(JsonCommand command); - CommandProcessingResult buyBackLoanByLoanId(JsonCommand command); + CommandProcessingResult buybackLoanByLoanId(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 043cdada8..1dc7c4196 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 @@ -50,6 +50,7 @@ import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.domain.ExternalAssetOwner; import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon; @@ -61,6 +62,7 @@ import org.springframework.transaction.annotation.Transactional; public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService { private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; + private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; private final ExternalAssetOwnerRepository externalAssetOwnerRepository; private final FromJsonHelper fromApiJsonHelper; private final LoanReadPlatformServiceCommon loanReadPlatformService; @@ -80,7 +82,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW @Override @Transactional - public CommandProcessingResult buyBackLoanByLoanId(JsonCommand command) { + public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) { Long loanId = command.getLoanId(); LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId); validateLoanStatus(loanIdAndExternalId); @@ -135,7 +137,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId()); if (latestTransferOptional.isPresent()) { ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get(); - ExternalTransferStatus latestTransferStatus = ExternalTransferStatus.valueOf(latestTransfer.getStatus()); + ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus(); if (latestTransferStatus.equals(ExternalTransferStatus.PENDING)) { throw new ExternalAssetOwnerInitiateTransferException( "External asset owner transfer is already in PENDING state for this loan."); @@ -154,7 +156,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW "This loan cannot be bought back, because it is not owned by an external asset owner"); } else { ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get(); - ExternalTransferStatus latestTransferStatus = ExternalTransferStatus.valueOf(latestTransfer.getStatus()); + ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus(); if (latestTransferStatus.equals(ExternalTransferStatus.BUYBACK)) { throw new ExternalAssetOwnerInitiateTransferException( "External asset owner transfer is already in BUYBACK state for this loan."); @@ -173,7 +175,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW externalAssetOwnerTransfer.setOwnerId(owner.getId()); externalAssetOwnerTransfer.setOwner(owner); externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json)); - externalAssetOwnerTransfer.setStatus(status.name()); + externalAssetOwnerTransfer.setStatus(status); externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json)); externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json)); externalAssetOwnerTransfer.setEffectiveDateFrom(ThreadLocalContextUtil.getBusinessDate()); @@ -188,7 +190,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE)); - final Type typeOfMap = new TypeToken>() {}.getType(); + final Type typeOfMap = new TypeToken>() { + + }.getType(); fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, apiRequestBodyAsJson, requestParameters); final List dataValidationErrors = new ArrayList<>(); diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index c7fa915cc..6ce35e811 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -27,4 +27,5 @@ + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml new file mode 100644 index 000000000..b35a8cf3b --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..e4f8e7d9c --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java @@ -0,0 +1,307 @@ +/** + * 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.cob.loan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.investor.data.ExternalTransferStatus; +import org.apache.fineract.investor.data.ExternalTransferSubStatus; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +public class LoanAccountOwnerTransferBusinessStepTest { + + public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31); + private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault()); + @Mock + private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; + @Mock + private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; + private LoanAccountOwnerTransferBusinessStep underTest; + + @BeforeEach + public void setUp() { + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate))); + underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository, + externalAssetOwnerTransferLoanMappingRepository); + } + + @Test + public void givenLoanNoTransfer() { + // 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); + // then + verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + assertEquals(processedLoan, loanForProcessing); + } + + @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() { + // 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(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE); + 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 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()); + verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + } + + @Test + public void givenLoanTwoTransferSameDay() { + // 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.BUYBACK); + List response = List.of(firstResponseItem, secondResponseItem); + when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) + .thenReturn(response); + ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransfer.class); + // 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(4)).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.CANCELLED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus()); + assertEquals(ExternalTransferSubStatus.SAMEDAY_TRANSFERS, + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSubStatus()); + 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(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getOwner(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getOwner()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getExternalId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getExternalId()); + assertEquals(ExternalTransferStatus.CANCELLED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getStatus()); + assertEquals(ExternalTransferSubStatus.SAMEDAY_TRANSFERS, + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getSubStatus()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getSettlementDate()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getLoanId(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getLoanId()); + assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getPurchasePriceRatio(), + externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getPurchasePriceRatio()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateFrom()); + assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo()); + + assertEquals(processedLoan, loanForProcessing); + } + + @Test + public void givenLoanBuyback() { + // 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.ACTIVE); + 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); + ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransfer.class); + when(externalAssetOwnerTransferRepository.save(firstResponseItem)).thenReturn(firstResponseItem); + when(externalAssetOwnerTransferRepository.save(secondResponseItem)).thenReturn(secondResponseItem); + // 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()); + verify(secondResponseItem).setEffectiveDateTo(secondResponseItem.getEffectiveDateFrom()); + 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 + 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); + ArgumentCaptor externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferLoanMapping.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); + // 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.ACTIVE, 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.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); + assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); + verify(externalAssetOwnerTransferLoanMappingRepository, times(1)) + .save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture()); + assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId()); + assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); + assertEquals(processedLoan, loanForProcessing); + } + + @Test + public void testGetEnumStyledNameSuccessScenario() { + final String actualEnumName = underTest.getEnumStyledName(); + assertNotNull(actualEnumName); + assertEquals("EXTERNAL_ASSET_OWNER_TRANSFER", actualEnumName); + } + + @Test + public void testGetHumanReadableNameSuccessScenario() { + final String actualEnumName = underTest.getHumanReadableName(); + assertNotNull(actualEnumName); + assertEquals("Execute external asset owner transfer", actualEnumName); + } +}