This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new d639b20af FINERACT-1926: Asset transfer to external owner COB business
step
d639b20af is described below
commit d639b20af16c61ebdec168cad29c78f88ab94405
Author: Adam Saghy <[email protected]>
AuthorDate: Fri May 26 16:40:28 2023 +0200
FINERACT-1926: Asset transfer to external owner COB business step
---
.../cob/exceptions/BusinessStepException.java | 0
.../loan/LoanAccountOwnerTransferBusinessStep.java | 195 +++++++++++++
.../investor/data/ExternalTransferSubStatus.java | 13 +-
.../domain/ExternalAssetOwnerTransfer.java | 27 +-
.../ExternalAssetOwnerTransferLoanMapping.java | 5 +-
...alAssetOwnerTransferLoanMappingRepository.java} | 11 +-
.../ExternalAssetOwnerTransferRepository.java | 1 +
...xternalAssetOwnerTransferNotFoundException.java | 33 +++
.../BuybackLoanFromExternalAssetOwnerHandler.java | 2 +-
.../service/ExternalAssetOwnersReadService.java | 1 +
.../service/ExternalAssetOwnersWriteService.java | 3 +-
.../ExternalAssetOwnersWriteServiceImpl.java | 14 +-
.../module/investor/module-changelog-master.xml | 1 +
.../module/investor/parts/0006_asset_schemas.xml | 49 ++++
.../LoanAccountOwnerTransferBusinessStepTest.java | 307 +++++++++++++++++++++
15 files changed, 628 insertions(+), 34 deletions(-)
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%
copy from
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
copy 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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer>
externalAssetOwnerTransferList) {
+ ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer =
buybackAsset(loan, settlementDate, externalAssetOwnerTransferList);
+ // TODO: trigger asset loan transfer executed event
+ }
+
+ private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final
LocalDate settlementDate,
+ List<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> 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-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
similarity index 73%
rename from
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
rename to
fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
index a11a5c57a..7899b49a8 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
+++
b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
@@ -16,15 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cob.exceptions;
+package org.apache.fineract.investor.data;
-public class BusinessStepException extends RuntimeException {
-
- public BusinessStepException(String message) {
- super(message);
- }
-
- public BusinessStepException(String message, Throwable t) {
- super(message, t);
- }
+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/service/ExternalAssetOwnersReadService.java
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
similarity index 61%
copy from
fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
copy to
fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
index 8f5f56d85..e7b28b4d8 100644
---
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
+++
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
@@ -16,12 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.investor.service;
+package org.apache.fineract.investor.domain;
-import java.util.List;
-import org.apache.fineract.investor.data.ExternalTransferData;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-public interface ExternalAssetOwnersReadService {
+public interface ExternalAssetOwnerTransferLoanMappingRepository extends
JpaRepository<ExternalAssetOwnerTransferLoanMapping, Long>,
+ JpaSpecificationExecutor<ExternalAssetOwnerTransferLoanMapping> {
- List<ExternalTransferData> retrieveTransferData(Long loanId, String
externalLoanId, String transferExternalId);
+ 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<ExternalAssetOwnerTransfer> 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<ExternalTransferData> 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<Map<String, Object>>()
{}.getType();
+ final Type typeOfMap = new TypeToken<Map<String, Object>>() {
+
+ }.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap,
apiRequestBodyAsJson, requestParameters);
final List<ApiParameterError> 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 @@
<include relativeToChangelogFile="true" file="parts/0003_asset_schemas.xml"/>
<include relativeToChangelogFile="true"
file="parts/0004_change_purchase_price_ratio_type.xml"/>
<include relativeToChangelogFile="true"
file="parts/0005_add_sale_and_buyback_command.xml"/>
+ <include relativeToChangelogFile="true" file="parts/0006_asset_schemas.xml"/>
</databaseChangeLog>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+
+ <changeSet author="fineract" id="1">
+ <addColumn tableName="m_external_asset_owner_transfer">
+ <column name="sub_status" type="VARCHAR(50)"/>
+ </addColumn>
+
+ <createIndex tableName="m_external_asset_owner_transfer"
+ indexName="external_asset_owner_transfer_sub_status">
+ <column name="sub_status"/>
+ </createIndex>
+ </changeSet>
+ <changeSet id="2" author="fineract">
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="owner_id" columnDataType="BIGINT"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="external_id" columnDataType="VARCHAR(100)"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="status" columnDataType="VARCHAR(50)"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="purchase_price_ratio" columnDataType="VARCHAR(50)"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="settlement_date" columnDataType="DATE"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="effective_date_from" columnDataType="DATE"/>
+ <addNotNullConstraint tableName="m_external_asset_owner_transfer"
columnName="effective_date_to" columnDataType="DATE"/>
+ </changeSet>
+
+ <changeSet id="3" author="fineract">
+ <addNotNullConstraint
tableName="m_external_asset_owner_transfer_loan_mapping"
columnName="owner_transfer_id" columnDataType="BIGINT"/>
+ </changeSet>
+</databaseChangeLog>
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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem,
secondResponseItem);
+
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class),
eq(Sort.by(Sort.Direction.ASC, "id"))))
+ .thenReturn(response);
+ ArgumentCaptor<ExternalAssetOwnerTransfer>
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<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem,
secondResponseItem);
+
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class),
eq(Sort.by(Sort.Direction.ASC, "id"))))
+ .thenReturn(response);
+ ArgumentCaptor<ExternalAssetOwnerTransfer>
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<ExternalAssetOwnerTransfer> 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<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
+
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class),
eq(Sort.by(Sort.Direction.ASC, "id"))))
+ .thenReturn(response);
+ ArgumentCaptor<ExternalAssetOwnerTransfer>
externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
+ .forClass(ExternalAssetOwnerTransfer.class);
+ ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping>
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);
+ }
+}