FINERACT-1926: Asset transfer to external owner COB business step

This commit is contained in:
Adam Saghy
2023-05-31 08:42:58 +02:00
parent b4cdb58fbe
commit d639b20af1
15 changed files with 670 additions and 19 deletions
@@ -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";
}
}
@@ -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
}
@@ -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)
@@ -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;
}
@@ -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<ExternalAssetOwnerTransferLoanMapping, Long>,
JpaSpecificationExecutor<ExternalAssetOwnerTransferLoanMapping> {
void deleteByLoanIdAndOwnerTransfer(Long loanId, ExternalAssetOwnerTransfer externalAssetOwnerTransfer);
}
@@ -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);
}
@@ -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);
}
}
@@ -34,6 +34,6 @@ public class BuybackLoanFromExternalAssetOwnerHandler implements NewCommandSourc
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnersWriteService.buyBackLoanByLoanId(command);
return externalAssetOwnersWriteService.buybackLoanByLoanId(command);
}
}
@@ -24,4 +24,5 @@ import org.apache.fineract.investor.data.ExternalTransferData;
public interface ExternalAssetOwnersReadService {
List<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId);
}
@@ -25,5 +25,6 @@ public interface ExternalAssetOwnersWriteService {
CommandProcessingResult saleLoanByLoanId(JsonCommand command);
CommandProcessingResult buyBackLoanByLoanId(JsonCommand command);
CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
}
@@ -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<>();
@@ -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>
@@ -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>
@@ -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);
}
}